mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Compare commits
238
Commits
@@ -1,41 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify (core, plugin, spi, probes)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Run verification gates
|
||||
run: ./gradlew :core:check :adapters:bukkit:plugin:test :adapters:bukkit:nms:v26_2_R1:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Run modded shared tests (Fabric)
|
||||
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Run modded shared tests (Forge)
|
||||
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Run modded shared tests (NeoForge)
|
||||
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Test artifact verifiers
|
||||
run: ./gradlew -p buildSrc test --console=plain --stacktrace
|
||||
- name: Verify Bukkit artifact
|
||||
run: ./gradlew verifyBukkitArtifact --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Verify modded artifacts
|
||||
run: ./gradlew verifyModdedArtifacts --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
@@ -71,3 +71,8 @@ AGENTS.md
|
||||
build-lt.sh
|
||||
BUILDS/
|
||||
.run/
|
||||
|
||||
# Version-content compat lane worktree. Generated, never source.
|
||||
.compatwt/
|
||||
|
||||
.worktrees/
|
||||
|
||||
@@ -14,7 +14,7 @@ Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
## Language and localization
|
||||
|
||||
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/core/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
|
||||
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
|
||||
|
||||
## Platforms
|
||||
|
||||
@@ -64,22 +64,22 @@ content selecting on `#minecraft:is_overworld` and friends.
|
||||
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
|
||||
the server. First boot performs no pack download. Run `/iris download pack=overworld`,
|
||||
`/iris download pack=underworld`, or `/iris download link=https://host/path/pack.zip`, waiting for
|
||||
each download to finish before starting another. The shipping Overworld declares Towns & Towers
|
||||
26.1 and Dungeons & Taverns 5.3.0. With the default automatic ingest enabled, the first restart
|
||||
after download installs those external datapacks and leaves admission restart-required; complete
|
||||
the ensuing clean restart so Minecraft loads them together with the Iris dimension types and
|
||||
biomes. If automatic ingest is disabled, run `/iris datapack ingest restart=true` instead and
|
||||
complete the restart it requests. Plain Spigot supports ordinary managed `/iris create`, but not
|
||||
each download to finish before starting another. The current built-in packs declare no external
|
||||
datapack imports. Restart after download so Minecraft loads their dimension types and biomes.
|
||||
Custom packs that declare `datapackImports` must complete their external-datapack installation and
|
||||
registry restart first. Bukkit accepts Modrinth, direct HTTP(S), and absolute local `file:` ZIP URLs;
|
||||
ZIPs under `plugins/Iris/datapacks/imports/` are discovered for every Iris dimension. Plain Spigot
|
||||
supports ordinary managed `/iris create`, but not
|
||||
the early-bootstrap `/iris replace` path for canonical Overworld, Nether, or End slots.
|
||||
|
||||
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
|
||||
self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only
|
||||
packs already on disk and never accesses the network. `/iris download` installs a pack atomically
|
||||
without stopping the server. Before loading the shipping Overworld, manually place the exact
|
||||
compatible Towns & Towers 26.1 and Dungeons & Taverns 5.3.0 archives in that save's `datapacks/`
|
||||
directory; modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart
|
||||
once only after the Iris pack and both external datapacks are present. Packs register their custom
|
||||
dimension types, height ranges, biomes, and external structure keys during that boot; worlds created
|
||||
without stopping the server. The current built-in packs declare no external datapack imports.
|
||||
Custom packs that declare them require compatible archives in that save's `datapacks/` directory;
|
||||
modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart only after
|
||||
the Iris pack and all of its inputs are present. Packs register their custom dimension types,
|
||||
height ranges, biomes, and external structure keys during that boot; worlds created
|
||||
before it run with fallback registry data.
|
||||
|
||||
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
|
||||
@@ -184,6 +184,19 @@ returns `---`. A real zero returns `0`.
|
||||
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
|
||||
| `%iris_world.biome%` | Surface biome display name at the player, e.g. `Hot Desert Dunes` |
|
||||
| `%iris_world.biome-key%` | Surface biome load key, e.g. `desert/hot-dunes` |
|
||||
| `%iris_world.biome-custom%` | `true` when the surface biome defines custom derivatives, otherwise `false` |
|
||||
| `%iris_world.biome-custom-id%` | Authored `customDerivitives[].id` when exactly one derivative is defined, for example `golden-dunes`; `---` for none or multiple |
|
||||
| `%iris_world.biome-custom-ids%` | All authored custom derivative IDs, comma-and-space separated in definition order; `---` for none |
|
||||
| `%iris_world.biome-custom-key%` | Namespaced Minecraft registry key for the sole custom derivative, such as `iris:biomes/<hash>`; `---` for none, multiple, or unavailable mapping |
|
||||
| `%iris_world.biome-custom-keys%` | Registry keys for all custom derivatives in definition order, comma-and-space separated; `---` for none or any unavailable mapping |
|
||||
| `%iris_world.biome-custom-count%` | Number of authored custom derivatives; `0` for a vanilla-only biome |
|
||||
| `%iris_world.biome-derivative%` | Configured base derivative registry key, for example `minecraft:desert` |
|
||||
| `%iris_world.biome-vanilla-derivative%` | Effective vanilla derivative registry key used when a vanilla biome is required |
|
||||
| `%iris_world.biome-type%` | Inferred biome category: `land`, `sea`, `shore`, or `cave`; `---` when unavailable |
|
||||
| `%iris_world.min-height%` | World minimum absolute Y, inclusive |
|
||||
| `%iris_world.max-height%` | World maximum absolute Y, exclusive |
|
||||
| `%iris_world.height%` | World vertical span, equal to maximum minus minimum height |
|
||||
| `%iris_world.fluid-height%` | Dimension fluid level in absolute world Y; this is configuration, not a measurement of water at the player |
|
||||
| `%iris_world.region%` | Region display name at the player |
|
||||
| `%iris_world.region-key%` | Region load key |
|
||||
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
|
||||
@@ -197,7 +210,9 @@ returns `---`. A real zero returns `0`.
|
||||
| `%iris_pregen.chunks-per-second%` | Current rate |
|
||||
| `%iris_pregen.paused%` | `true` while the job is paused |
|
||||
|
||||
The world values are the surface reading at the player's block column. Walking refreshes them at most
|
||||
Custom derivative IDs are authored metadata. A singular ID or registry key is available only when the biome defines one derivative; plural keys list all definitions. Saved columns retain their historical biome definitions.
|
||||
|
||||
Biome and region values describe the surface at the player's block column. Height values describe world configuration. Walking refreshes them at most
|
||||
once per second per player, so a whole board of `world.*` keys costs one refresh per player per
|
||||
second no matter how many of them are on it, and a value may lag a sprinting player by up to a
|
||||
second. A jump that is not walking - joining, respawning, changing worlds, stepping through a portal,
|
||||
@@ -237,7 +252,7 @@ Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everyth
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
builds every platform artifact into `dist/`:
|
||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
@@ -250,9 +265,11 @@ Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `build
|
||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a
|
||||
consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo;
|
||||
override with `-Plocation=/path/to/consumers`.
|
||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
||||
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
|
||||
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
|
||||
changes only the consumer directory. Both tasks copy all four verified platform jars into
|
||||
`../PluginOuts/`.
|
||||
|
||||
If you need help compiling as a developer or contributor, ask in the Discord.
|
||||
|
||||
|
||||
-743
@@ -1,743 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
private static final int NOISE_BIOME_CACHE_MAX = 262144;
|
||||
private static final int STRONGHOLD_RING_SEARCH_Y = 0;
|
||||
private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112;
|
||||
private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4;
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
private volatile int cacheRuntimeId;
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
this.seed = seed;
|
||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
this.cacheRuntimeId = engine.getCacheID();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, i.getStructureDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ i.getStructureDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
|
||||
}
|
||||
biomes.add(vanillaHolder);
|
||||
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder == null) {
|
||||
throw new IllegalStateException("Iris custom structure biome '"
|
||||
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
|
||||
}
|
||||
biomes.add(customHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (biomes.isEmpty()) {
|
||||
throw new IllegalStateException("Iris pack '" + engine.getName()
|
||||
+ "' has no registered structure biomes");
|
||||
}
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
o = invokeFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
|
||||
+ " from " + source.getClass().getName() + " on this server version");
|
||||
}
|
||||
|
||||
private static Object fieldFor(Class<?> returns, Object in) {
|
||||
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
|
||||
Object o = fieldForClass(returns, sourceType, in);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object invokeFor(Class<?> returns, Object in) {
|
||||
for (Method i : in.getClass().getMethods()) {
|
||||
if (i.getReturnType().equals(returns)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
|
||||
return i.invoke(in);
|
||||
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
|
||||
+ i.getName() + "() for " + returns.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
|
||||
for (Field i : sourceType.getDeclaredFields()) {
|
||||
if (i.getType().equals(returnType)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
|
||||
return (T) i.get(in);
|
||||
} catch (IllegalAccessException | RuntimeException e) {
|
||||
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
|
||||
+ i.getName() + " for " + returnType.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return possibleStructureBiomes().stream();
|
||||
}
|
||||
|
||||
Set<Holder<Biome>> possibleStructureBiomes() {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_possible_biomes");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris possible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
}
|
||||
Registry<Biome> customRegistry = ((RegistryAccess) getFor(
|
||||
RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
|
||||
.lookup(Registries.BIOME).orElse(null);
|
||||
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
|
||||
}
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
KMap<String, Holder<Biome>> m = new KMap<>();
|
||||
if (customRegistry == null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(j.getId(), fallback);
|
||||
}
|
||||
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
m.put(j.getId(), holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
|
||||
if (customRegistry == null || registry == null) {
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_spawn_biome");
|
||||
if (lease == null) {
|
||||
return null;
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
return null;
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return vanillaSpawnBiomes.get(biome.value());
|
||||
}
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
|
||||
if (access == null) {
|
||||
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
|
||||
}
|
||||
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
if (isGuaranteedSurfaceBiome(y)) {
|
||||
return getSurfaceStructureBiomeHolder(x, z);
|
||||
}
|
||||
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<BlockPos, Holder<Biome>> findBiomeHorizontal(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int searchRadius,
|
||||
Predicate<Holder<Biome>> allowed,
|
||||
RandomSource random,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
int quartStep = horizontalBiomeSearchQuartStep(y, searchRadius);
|
||||
if (quartStep == 1) {
|
||||
return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler);
|
||||
}
|
||||
return super.findBiomeHorizontal(
|
||||
x, y, z, searchRadius, quartStep, allowed, random, false, sampler);
|
||||
}
|
||||
|
||||
static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) {
|
||||
return blockY == STRONGHOLD_RING_SEARCH_Y && searchRadius == STRONGHOLD_RING_SEARCH_RADIUS
|
||||
? STRONGHOLD_RING_SEARCH_QUART_STEP
|
||||
: 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Holder<Biome>> getBiomesWithin(int x, int y, int z, int radius, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_biomes_within");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
boolean monumentQuery = radius == 29
|
||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY)) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
int maxQuartX = QuartPos.fromBlock(x + radius);
|
||||
int minQuartZ = QuartPos.fromBlock(z - radius);
|
||||
int maxQuartZ = QuartPos.fromBlock(z + radius);
|
||||
int columns = (maxQuartX - minQuartX + 1) * (maxQuartZ - minQuartZ + 1);
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiomeHolder(quartX, quartZ));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
if (engine == null || engine.isClosed() || engine.getComplex() == null) {
|
||||
return false;
|
||||
}
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = (quartY << 2) - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
return internalY > caveSwitchInternalY;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveSurfaceStructureBiomeHolder(int x, int z) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null) {
|
||||
throw new IllegalStateException("Iris returned no surface structure biome at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
Holder<Biome> holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey());
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ irisBiome.getStructureDerivativeKey() + "' is not registered at block "
|
||||
+ blockX + "," + blockZ);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
prepareVisibleBiomeBatch();
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
void prepareVisibleBiomeBatch() {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
Climate.Sampler sampler,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
noiseBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private GenerationSessionLease tryAcquireGenerationLease(String operation) {
|
||||
if (engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return engine.acquireGenerationLease(operation);
|
||||
} catch (GenerationSessionException e) {
|
||||
if (engine.isClosing() || e.isExpectedTeardown()) {
|
||||
return null;
|
||||
}
|
||||
throw new IllegalStateException("Iris biome source could not acquire generation session for "
|
||||
+ operation + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRuntimeAvailable() {
|
||||
return !engine.isClosed() && engine.getComplex() != null;
|
||||
}
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
biomeCustomRegistry, engine, fallbackBiome);
|
||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine);
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
cacheRuntimeId = runtimeId;
|
||||
}
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveBiomeHolder(
|
||||
biomeRegistry, resolution.irisBiome.getStructureDerivativeKey());
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Iris structure biome derivative '"
|
||||
+ resolution.irisBiome.getStructureDerivativeKey() + "' is not registered at block "
|
||||
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
return resolveBiomeResolution(x, y, z, null);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean underground = false;
|
||||
IrisBiome irisBiome;
|
||||
if (deepUnderground) {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createBiomeResolution(irisBiome, underground, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
private BiomeResolution createBiomeResolution(
|
||||
IrisBiome irisBiome,
|
||||
boolean underground,
|
||||
int blockX,
|
||||
int blockY,
|
||||
int blockZ
|
||||
) {
|
||||
RNG noiseRng = new RNG(seed
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
if (fallbackBiome != null) {
|
||||
return fallbackBiome;
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static long packColumnKey(int x, int z) {
|
||||
return ((long) x << 32) ^ ((long) z & 4294967295L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
||||
);
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
|
||||
if (optionalReferenceHolder.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return optionalReferenceHolder.get();
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String biomeKey) {
|
||||
if (registry == null || biomeKey == null || biomeKey.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(biomeKey);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
return registry.get(ResourceKey.create(Registries.BIOME, identifier)).orElse(null);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
|
||||
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
|
||||
Holder<Biome> vanilla = firstHolder(registry);
|
||||
if (vanilla != null) {
|
||||
return vanilla;
|
||||
}
|
||||
|
||||
return firstHolder(customRegistry);
|
||||
}
|
||||
|
||||
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Biome biome : registry) {
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
|
||||
if (optionalHolder.isPresent()) {
|
||||
return optionalHolder.get();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1280
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,8 +1,8 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.engine.framework.NativeStructureFrequencyScale;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.Vec3i;
|
||||
+98
-57
@@ -1,12 +1,16 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDecorationStep;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImportedFeatureControl;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorationStep;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import it.unimi.dsi.fastutil.ints.IntArraySet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
@@ -26,6 +30,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
@@ -38,6 +43,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/**
|
||||
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
|
||||
@@ -55,9 +62,7 @@ final class ImportedFeatureStage {
|
||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||
|
||||
private final Engine engine;
|
||||
private volatile FeatureTable featureTable;
|
||||
private volatile IrisDimension inertDimension;
|
||||
private volatile int settledRuntimeId;
|
||||
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
|
||||
|
||||
ImportedFeatureStage(Engine engine) {
|
||||
this.engine = engine;
|
||||
@@ -69,7 +74,8 @@ final class ImportedFeatureStage {
|
||||
* does today.
|
||||
*/
|
||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||
FeatureTable table = featureTable;
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -80,50 +86,46 @@ final class ImportedFeatureStage {
|
||||
/**
|
||||
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
||||
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
||||
*
|
||||
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
|
||||
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
|
||||
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
|
||||
*/
|
||||
void prepare(WorldGenLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (settled(dimension, runtimeId)) {
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (settled(dimension, runtimeId)) {
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
build(level, dimension);
|
||||
settledRuntimeId = runtimeId;
|
||||
runtimeStates.put(runtimeId, build(level, dimension));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settled(IrisDimension dimension, int runtimeId) {
|
||||
if (settledRuntimeId != runtimeId) {
|
||||
return false;
|
||||
}
|
||||
FeatureTable current = featureTable;
|
||||
if (current != null && current.dimension() == dimension) {
|
||||
return true;
|
||||
}
|
||||
return inertDimension == dimension;
|
||||
void evictRuntime(int runtimeId) {
|
||||
runtimeStates.remove(runtimeId);
|
||||
}
|
||||
|
||||
private void build(WorldGenLevel level, IrisDimension dimension) {
|
||||
private RuntimeFeatureState currentState() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
|
||||
return settled(state, dimension) ? state : null;
|
||||
}
|
||||
|
||||
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
|
||||
return state != null && state.dimension() == dimension;
|
||||
}
|
||||
|
||||
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
|
||||
IrisImportedFeatureControl control;
|
||||
try {
|
||||
control = NativeFeatureGenerationPolicy.control(engine);
|
||||
} catch (RuntimeException error) {
|
||||
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
||||
+ error);
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (!control.shouldGenerateFeatures()) {
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
FeatureTable built;
|
||||
try {
|
||||
@@ -131,23 +133,15 @@ final class ImportedFeatureStage {
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||
+ ": feature table construction failed: " + error);
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (built == null) {
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
featureTable = built;
|
||||
inertDimension = null;
|
||||
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
||||
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
||||
+ " custom-biome derivative maps");
|
||||
}
|
||||
|
||||
private void markInert(IrisDimension dimension) {
|
||||
featureTable = null;
|
||||
inertDimension = dimension;
|
||||
return new RuntimeFeatureState(dimension, built);
|
||||
}
|
||||
|
||||
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
||||
@@ -196,8 +190,7 @@ final class ImportedFeatureStage {
|
||||
*/
|
||||
private Set<String> visibleBiomeKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||
addKey(keys, irisBiome.getDerivativeKey());
|
||||
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
||||
@@ -211,7 +204,7 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
addKey(keys, namespace + ":" + customBiome.getId());
|
||||
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
@@ -224,8 +217,7 @@ final class ImportedFeatureStage {
|
||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
||||
Map<String, Holder<Biome>> byKey) {
|
||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
@@ -244,7 +236,10 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
derivatives.put(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT), derivative);
|
||||
derivatives.put(
|
||||
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
|
||||
derivative
|
||||
);
|
||||
}
|
||||
}
|
||||
return derivatives;
|
||||
@@ -275,7 +270,8 @@ final class ImportedFeatureStage {
|
||||
* control is disabled or the table degraded.
|
||||
*/
|
||||
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
||||
FeatureTable table = featureTable;
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
@@ -286,14 +282,31 @@ final class ImportedFeatureStage {
|
||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table);
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
int staticMinY = engine.getMinHeight();
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
|
||||
IntBinaryOperator surfaceFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
|
||||
IntBinaryOperator floorFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
|
||||
? level
|
||||
: NativeStructureWorldgenAccess.create(
|
||||
level, centerPos,
|
||||
surfaceFirstFreeY,
|
||||
floorFirstFreeY,
|
||||
stackContext != null,
|
||||
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
|
||||
|
||||
try {
|
||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
|
||||
continue;
|
||||
}
|
||||
placeStep(level, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
|
||||
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
|
||||
random, decorationSeed, origin, stepIndex);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
@@ -344,12 +357,37 @@ final class ImportedFeatureStage {
|
||||
return id == null ? feature.toString() : id.toString();
|
||||
}
|
||||
|
||||
private Set<Holder<Biome>> chunkBiomes(WorldGenLevel level, SectionPos sectionPos, FeatureTable table) {
|
||||
private Set<Holder<Biome>> chunkBiomes(
|
||||
WorldGenLevel level,
|
||||
SectionPos sectionPos,
|
||||
FeatureTable table,
|
||||
DimensionStackContext stackContext
|
||||
) {
|
||||
List<Holder<Biome>> collected = new ArrayList<>();
|
||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
for (LevelChunkSection section : neighbour.getSections()) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
LevelChunkSection[] sections = neighbour.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (stackContext == null) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
continue;
|
||||
}
|
||||
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
for (int quartY = 0; quartY < 4; quartY++) {
|
||||
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
|
||||
for (int quartX = 0; quartX < 4; quartX++) {
|
||||
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
|
||||
for (int quartZ = 0; quartZ < 4; quartZ++) {
|
||||
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
|
||||
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
|
||||
blockX, internalY, blockZ);
|
||||
if (layer.terrainContext().isSelfReferencing()) {
|
||||
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||
@@ -396,4 +434,7 @@ final class ImportedFeatureStage {
|
||||
List<FeatureSorter.StepFeatureData> steps,
|
||||
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
||||
}
|
||||
|
||||
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
|
||||
}
|
||||
}
|
||||
+530
-108
@@ -1,18 +1,35 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.nativegen.NativeTransitionColumn;
|
||||
import art.arcane.iris.nativegen.NativeTerrainHeightCache;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import java.util.Optional;
|
||||
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
|
||||
import java.util.function.LongPredicate;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineLifecycleTasks;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipStore;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
|
||||
@@ -30,10 +47,10 @@ import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.platform.reflect.WrappedField;
|
||||
import art.arcane.iris.platform.reflect.WrappedReturningMethod;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -98,6 +115,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
@@ -106,7 +124,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -114,8 +132,16 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate {
|
||||
private static final Set<Heightmap.Types> AUTHORING_HEIGHTMAPS = Set.of(
|
||||
Heightmap.Types.WORLD_SURFACE_WG,
|
||||
Heightmap.Types.OCEAN_FLOOR_WG,
|
||||
Heightmap.Types.WORLD_SURFACE,
|
||||
Heightmap.Types.OCEAN_FLOOR,
|
||||
Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private static final Runnable NO_OP = () -> {
|
||||
@@ -129,9 +155,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private final int runtimeHeight;
|
||||
private final int runtimeSeaLevel;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile IrisDimension spawnTableDimension;
|
||||
private volatile int spawnTableRuntimeId;
|
||||
private final ImportedFeatureStage importedFeatures;
|
||||
private final NativeTerrainHeightCache terrainHeights = new NativeTerrainHeightCache();
|
||||
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
private volatile StructureStepCache structureStepCache;
|
||||
@@ -154,9 +179,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
this.runtimeMinY = level.getMinY();
|
||||
this.runtimeHeight = level.getHeight();
|
||||
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
||||
if (engine instanceof IrisEngine irisEngine) {
|
||||
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
|
||||
}
|
||||
installNativeStructureVolumeIndex(level);
|
||||
}
|
||||
|
||||
private void evictRuntimeCaches(int runtimeId) {
|
||||
terrainHeights.evictRuntime(runtimeId);
|
||||
importedFeatures.evictRuntime(runtimeId);
|
||||
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
ReachableStructureCache reachable = reachableStructureCache;
|
||||
if (reachable != null && reachable.runtimeId() == runtimeId) {
|
||||
reachableStructureCache = null;
|
||||
}
|
||||
StructureStepCache steps = structureStepCache;
|
||||
if (steps != null && steps.runtimeId() == runtimeId) {
|
||||
structureStepCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void installNativeStructureVolumeIndex(ServerLevel level) {
|
||||
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
||||
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
|
||||
@@ -182,7 +224,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
|
||||
return null;
|
||||
}
|
||||
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
SectionPos.blockToSectionCoord(pos.getX()),
|
||||
SectionPos.blockToSectionCoord(pos.getZ()),
|
||||
"bukkit_nms_structure_locate");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||
NativeStructureVanillaLocator.Candidate nativeCandidate =
|
||||
@@ -361,9 +408,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = access.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||
@@ -380,6 +434,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
));
|
||||
adjustGeneratedStructures(
|
||||
registryAccess, access, previousStarts, configuredStarts, templateManager);
|
||||
if (route != null) {
|
||||
access.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"),
|
||||
PersistentDataType.LONG, route.activation().activationId());
|
||||
access.markUnsaved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,15 +454,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
NativeStructureOwnershipStore.discard(
|
||||
engine, structureId, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
continue;
|
||||
}
|
||||
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||
IrisNativeStructureDecision decision;
|
||||
try {
|
||||
@@ -428,7 +500,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
undergroundStep,
|
||||
decision.preserveSourceY(),
|
||||
decision.yBand(),
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||
start, structure, start.getReferences(),
|
||||
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()),
|
||||
@@ -630,7 +702,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (runtimeLevel.getChunkSource().getGenerator() != this) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
NativeStructureReferenceRepair.createReferences(
|
||||
@@ -640,7 +716,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
customBiomeSource.prepareVisibleBiomeBatch();
|
||||
@@ -655,31 +735,69 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_build_surface");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_build_surface");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
|
||||
return;
|
||||
}
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_apply_carvers");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_apply_carvers");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
|
||||
return;
|
||||
}
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
|
||||
ichunkaccess.getPos(),
|
||||
"bukkit_nms_chunk_pipeline");
|
||||
GenerationSessionLease lease;
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_chunk_pipeline");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route;
|
||||
try {
|
||||
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
||||
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
|
||||
} catch (RuntimeException | Error failure) {
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
GenerationSessionLease lease;
|
||||
try {
|
||||
CompletableFuture<ChunkAccess> pipeline = delegate
|
||||
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
|
||||
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
||||
} catch (RuntimeException | Error failure) {
|
||||
closeHistoryRoute(route, failure);
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<ChunkAccess> delegatePipeline;
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
delegatePipeline = delegate.fillFromNoise(
|
||||
blender, randomstate, structuremanager, ichunkaccess);
|
||||
}
|
||||
CompletableFuture<ChunkAccess> pipeline = delegatePipeline
|
||||
.thenApply(filled -> {
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
||||
openHistoryRuntimeScope(route);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
claimGeneratedSemantics(route, filled, engine.getMinHeight());
|
||||
persistNaturalTerrain(filled, route);
|
||||
primeWorldgenHeightmaps(filled);
|
||||
return filled;
|
||||
}
|
||||
@@ -687,24 +805,95 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
||||
pipeline.whenComplete((filled, failure) -> {
|
||||
boolean cancelled = isCancellationFailure(failure);
|
||||
lease.close();
|
||||
stage.close();
|
||||
if (failure == null) {
|
||||
Throwable completionFailure = closeNoisePipelineResources(
|
||||
failure, lease, route, stage);
|
||||
if (completionFailure == null) {
|
||||
completion.complete(filled);
|
||||
} else if (cancelled) {
|
||||
completion.cancel(false);
|
||||
} else {
|
||||
completion.completeExceptionally(failure);
|
||||
completion.completeExceptionally(completionFailure);
|
||||
}
|
||||
});
|
||||
lease.detachThread();
|
||||
if (route != null) {
|
||||
route.detachThread();
|
||||
}
|
||||
return completion;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
lease.close();
|
||||
closeHistoryRoute(route, failure);
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void persistNaturalTerrain(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) {
|
||||
if (route == null || route.naturalTerrain().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
|
||||
chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"), PersistentDataType.BYTE_ARRAY,
|
||||
NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId()));
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to persist natural terrain for chunk " + chunk.getPos(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable closeNoisePipelineResources(
|
||||
Throwable failure,
|
||||
GenerationSessionLease lease,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
BukkitChunkGenerator.GenerationStagePermit stage
|
||||
) {
|
||||
Throwable result = failure;
|
||||
try {
|
||||
lease.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
try {
|
||||
if (route != null) {
|
||||
route.close();
|
||||
}
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
try {
|
||||
stage.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void closeHistoryRoute(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
Throwable failure
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
|
||||
if (failure == null) {
|
||||
return closeFailure;
|
||||
}
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static boolean isCancellationFailure(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current instanceof CompletionException && current.getCause() != null) {
|
||||
@@ -714,9 +903,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
|
||||
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
|
||||
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||
primeAuthoringHeightmaps(chunkAccess);
|
||||
return;
|
||||
}
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
}
|
||||
|
||||
static void primeAuthoringHeightmaps(ChunkAccess chunkAccess) {
|
||||
Heightmap.primeHeightmaps(chunkAccess, AUTHORING_HEIGHTMAPS);
|
||||
}
|
||||
|
||||
private IntBinaryOperator worldgenSurfaceHeight() {
|
||||
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
@@ -725,9 +922,47 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return (x, z) -> engine.getHeight(x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator hostWorldgenSurfaceHeight() {
|
||||
return (x, z) -> Engine.hostHeight(engine, x, z, false) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
private IntBinaryOperator hostWorldgenFloorHeight() {
|
||||
return (x, z) -> Engine.hostHeight(engine, x, z, true) + runtimeMinY + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
return EngineLifecycleTasks.call(engine, "bukkit_nms_mob_spawns",
|
||||
() -> getAdmittedMobsAt(holder, structuremanager, enumcreaturetype, blockposition),
|
||||
WeightedList.of(List.of()));
|
||||
}
|
||||
|
||||
private WeightedList<MobSpawnSettings.SpawnerData> getAdmittedMobsAt(
|
||||
Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(
|
||||
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
|
||||
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
|
||||
if (selection.mode() == NativeBiomeSpawnSelection.Mode.LOADING) {
|
||||
return WeightedList.of(List.of());
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns")) {
|
||||
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
|
||||
}
|
||||
}
|
||||
|
||||
private WeightedList<MobSpawnSettings.SpawnerData> getMobsAtWithActiveRuntime(
|
||||
Holder<Biome> holder,
|
||||
StructureManager structuremanager,
|
||||
MobCategory enumcreaturetype,
|
||||
BlockPos blockposition,
|
||||
NativeBiomeSpawnSelection selection
|
||||
) {
|
||||
Holder<Biome> vanillaSpawnBiome = switch (selection.mode()) {
|
||||
case RETAINED -> customBiomeSource.getRetainedVanillaSpawnBiome(selection.derivativeKey());
|
||||
case CURRENT -> customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
case NONE, LOADING -> null;
|
||||
};
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
}
|
||||
@@ -747,18 +982,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
IrisDimension spawnDimension = engine.getDimension();
|
||||
int spawnRuntimeId = engine.getCacheID();
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
synchronized (mergedSpawnTables) {
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
mergedSpawnTables.clear();
|
||||
spawnTableDimension = spawnDimension;
|
||||
spawnTableRuntimeId = spawnRuntimeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), vanillaSpawnBiome.value(), enumcreaturetype);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
@@ -774,19 +999,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||
// feature-order cycle is reported once and degraded to features-off.
|
||||
importedFeatures.prepare(generatoraccessseed);
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||
// dimension set importedFeatures.enabled.
|
||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
boolean flatStudioTerrain = platformGenerator != null && platformGenerator.usesFlatStudioTerrain();
|
||||
if (!flatStudioTerrain && !ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
}
|
||||
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)
|
||||
&& NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) {
|
||||
if (!flatStudioTerrain) {
|
||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||
// feature-order cycle is reported once and degraded to features-off.
|
||||
importedFeatures.prepare(generatoraccessseed);
|
||||
}
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
if (!flatStudioTerrain) {
|
||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||
// dimension set importedFeatures.enabled.
|
||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||
}
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,6 +1078,15 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!isHistoricalStructureStart(world, start)
|
||||
&& !engine.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
continue;
|
||||
}
|
||||
NativeStructureOwnershipRecord ownership =
|
||||
NativeStructureOwnershipRecovery.resolve(
|
||||
engine, world.getLevel(), structureId, structure, start);
|
||||
@@ -867,25 +1116,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
try {
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, chunkPos, heightmapStarts, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
world, chunkPos, heightmapStarts,
|
||||
hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight());
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"heightmap priming", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
Predicate<BlockPos> protectedPosition = NativeGenerationWriteGuard.protectedPositions(
|
||||
staticObjects, engine.getDimensionStackContext(), engine.getMinHeight());
|
||||
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, hostWorldgenSurfaceHeight(), hostWorldgenFloorHeight(),
|
||||
engine.getDimensionStackContext() != null,
|
||||
protectedPosition);
|
||||
try {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
world, chunk, area, vegetationTargets);
|
||||
boundedWorld, chunk, area, vegetationTargets);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
|
||||
chunkPos.x(), chunkPos.z(), error);
|
||||
}
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan vacuumFoundationPlan;
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan surfaceTerrainPlan;
|
||||
try {
|
||||
vacuumFoundationPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world, area, terrainTargets,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
surfaceTerrainPlan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
boundedWorld, area, terrainTargets,
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
surfaceTerrainPlan.primeHeightmaps(chunk);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain integration", nativeStructureBatchContext(placementGroups),
|
||||
@@ -893,7 +1151,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
try {
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
world, area, terrainTargets, this::resolvePaletteBlock);
|
||||
boundedWorld, area, terrainTargets, this::resolvePaletteBlock);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"terrain preparation", nativeStructureBatchContext(placementGroups),
|
||||
@@ -903,7 +1161,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
|
||||
try {
|
||||
for (NativePlacement placement : group.placements()) {
|
||||
placeVanillaStructure(world, structureManager, random, area, chunkPos,
|
||||
placeVanillaStructure(boundedWorld, structureManager, random, area, chunkPos,
|
||||
group.structureId(), placement.start(), placement.decision());
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
@@ -913,7 +1171,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
try {
|
||||
NativeStructureSurfaceFitter.repairVacuumFoundations(
|
||||
world, area, vacuumFoundationPlan);
|
||||
boundedWorld, area, surfaceTerrainPlan);
|
||||
} catch (Throwable error) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"foundation repair", nativeStructureBatchContext(placementGroups),
|
||||
@@ -921,6 +1179,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHistoricalStructureStart(WorldGenLevel world, StructureStart start) {
|
||||
ChunkPos origin = start.getChunkPos();
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) {
|
||||
return true;
|
||||
}
|
||||
ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
long activation = source.persistentDataContainer.getOrDefault(
|
||||
new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 0L);
|
||||
return NativeGenerationWriteGuard.isHistoricalStructure(engine, activation);
|
||||
}
|
||||
|
||||
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
|
||||
if (placementGroups.isEmpty()) {
|
||||
return "<no resolved native structures>";
|
||||
@@ -938,26 +1210,34 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
|
||||
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
|
||||
IrisNativeStructureDecision decision) {
|
||||
WorldGenLevel boundedWorld = NativeStructureWorldgenAccess.create(
|
||||
world, chunkPos, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||
WorldGenLevel boundedWorld = world instanceof NativeStructureWorldgenAccess ? world : NativeStructureWorldgenAccess.create(
|
||||
world,
|
||||
chunkPos,
|
||||
hostWorldgenSurfaceHeight(),
|
||||
hostWorldgenFloorHeight(),
|
||||
engine.getDimensionStackContext() != null,
|
||||
NativeGenerationWriteGuard.protectedPositions(
|
||||
engine.getDimension().getStaticObjectLayer(engine.getData()),
|
||||
engine.getDimensionStackContext(), engine.getMinHeight()));
|
||||
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
|
||||
try {
|
||||
NativeStructurePostProcessor.place(boundedWorld, structureManager, this, random, area, chunkPos,
|
||||
structureId, start, decision, this::resolvePaletteBlock,
|
||||
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
|
||||
(x, z) -> Engine.hostHeight(engine, x, z, true) + engine.getMinHeight());
|
||||
} finally {
|
||||
world.setCurrentlyGenerating(null);
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
int runtimeId = engine.getCacheID();
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
@@ -972,7 +1252,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -1004,12 +1284,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||
ChunkPos chunkPos = chunkAccess.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
|
||||
primeWorldgenHeightmaps(chunkAccess);
|
||||
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
@@ -1035,12 +1321,47 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
|
||||
return;
|
||||
}
|
||||
Holder<Biome> visibleBiome;
|
||||
if (engine.getDimensionStackContext() == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
} else {
|
||||
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
||||
center.getMinBlockX() + 8,
|
||||
center.getMinBlockZ() + 8);
|
||||
if (visibleBiome == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
}
|
||||
}
|
||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(long packedChunk) {
|
||||
return !engine.isClosing() && !engine.isClosed()
|
||||
&& engine.getComplex().allowsMantleChunkWrite(ChunkPos.getX(packedChunk), ChunkPos.getZ(packedChunk));
|
||||
}
|
||||
|
||||
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
|
||||
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||
return false;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
return NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, stage)
|
||||
|| engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
|
||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||
@@ -1073,22 +1394,68 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||
return platformGenerator.getAuthoringBaseHeight();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
i, j, "bukkit_nms_base_height");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
return levelheightaccessor.getMinY()
|
||||
+ engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()))
|
||||
+ 1;
|
||||
NativeTerrainHeightCache.Query query = new NativeTerrainHeightCache.Query(
|
||||
engine.getCacheID(), i, j, heightmap_type,
|
||||
levelheightaccessor.getMinY(), levelheightaccessor.getHeight());
|
||||
OptionalInt resolved = terrainHeights.resolvedHeight(query,
|
||||
() -> resolvedBaseHeight(i, j, heightmap_type, levelheightaccessor));
|
||||
if (resolved.isPresent()) {
|
||||
return resolved.getAsInt();
|
||||
}
|
||||
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
int height = engine.getDimensionStackContext() == null
|
||||
? engine.getHeight(i, j, ignoreFluid)
|
||||
: Engine.hostHeight(engine, i, j, ignoreFluid);
|
||||
return levelheightaccessor.getMinY() + height + 1;
|
||||
} catch (GenerationSessionException e) {
|
||||
throw new IllegalStateException("Iris base height query could not acquire its engine runtime.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private OptionalInt resolvedBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor) {
|
||||
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
|
||||
return resolved.isPresent()
|
||||
? OptionalInt.of(NativeTransitionColumn.height(resolved.get(), type, heightAccessor))
|
||||
: OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
int floorIndex = platformGenerator.getAuthoringFloorY() - levelheightaccessor.getMinY();
|
||||
BlockState floor = platformGenerator.isJigsawStudioActive() && JigsawStudioGenerator.isLightFloor(i, j)
|
||||
? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: Blocks.POLISHED_DEEPSLATE.defaultBlockState();
|
||||
for (int index = 0; index < column.length; index++) {
|
||||
column[index] = index == floorIndex
|
||||
? floor
|
||||
: Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
i, j, "bukkit_nms_base_column");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
int block = engine.getHeight(i, j, true);
|
||||
int water = engine.getHeight(i, j, false);
|
||||
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(i, j);
|
||||
if (resolved.isPresent()) {
|
||||
return NativeTransitionColumn.column(resolved.get(), levelheightaccessor);
|
||||
}
|
||||
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||
int block = dimensionStack
|
||||
? Engine.hostHeight(engine, i, j, true)
|
||||
: engine.getHeight(i, j, true);
|
||||
int water = dimensionStack
|
||||
? Engine.hostHeight(engine, i, j, false)
|
||||
: engine.getHeight(i, j, false);
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
for (int k = 0; k < column.length; k++) {
|
||||
if (k <= block) {
|
||||
@@ -1113,25 +1480,79 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
String operation
|
||||
) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
if (router == null) {
|
||||
if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " has no generation-history runtime router.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return router.openRoute(chunkX, chunkZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not route generation history for chunk "
|
||||
+ chunkX + "," + chunkZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
String operation
|
||||
) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not route generation history at "
|
||||
+ blockX + "," + blockZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||
) {
|
||||
return route == null ? null : route.openRuntimeScope();
|
||||
}
|
||||
|
||||
private static void claimGeneratedSemantics(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
ChunkAccess chunk,
|
||||
int minimumY
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
route.claimGeneratedSemantics((x, y, z) -> {
|
||||
BlockState state = chunk.getBlockState(position.set(x, minimumY + y, z));
|
||||
return state.isAir() || state.liquid();
|
||||
});
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
|
||||
return platformGenerator == null
|
||||
? BukkitChunkGenerator.GenerationStagePermit.noop()
|
||||
: platformGenerator.acquireGenerationStage(operation);
|
||||
}
|
||||
|
||||
private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage(
|
||||
ChunkPos chunkPos,
|
||||
String operation
|
||||
) {
|
||||
return platformGenerator == null
|
||||
? BukkitChunkGenerator.GenerationStagePermit.noop()
|
||||
: platformGenerator.acquireNoiseGenerationStage(
|
||||
engine,
|
||||
chunkPos.x(),
|
||||
chunkPos.z(),
|
||||
operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Identifier> getTypeNameForDataFixer() {
|
||||
return delegate.getTypeNameForDataFixer();
|
||||
@@ -1184,13 +1605,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
private record SpawnTableKey(int runtimeId, Biome biome, Biome vanillaBiome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
private record StructureStepCache(int runtimeId, Registry<Structure> registry,
|
||||
List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
record StudioStructureState(
|
||||
+103
-330
@@ -1,46 +1,47 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.core.nms.DatapackStructureScopeResult;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.platform.bukkit.nms.DatapackStructureScopeResult;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.core.nms.MinecraftVersion;
|
||||
import art.arcane.iris.core.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.iris.platform.bukkit.nms.MinecraftVersion;
|
||||
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.platform.bukkit.nms.container.BiomeColor;
|
||||
import art.arcane.iris.platform.bukkit.nms.container.Pair;
|
||||
import art.arcane.iris.platform.bukkit.nms.container.BlockProperty;
|
||||
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.generation.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.chunk.TerrainChunk;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
|
||||
import art.arcane.iris.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.nativegen.NativeStructureFactory;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.Vector3d;
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
|
||||
import art.arcane.iris.world.storage.region.NBTWorld;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAChunkBiomeContainer;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAGlobalPalette;
|
||||
@@ -51,14 +52,8 @@ import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAPalettedContainer;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
@@ -91,7 +86,6 @@ import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
@@ -104,7 +98,6 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
@@ -123,11 +116,11 @@ import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.TreeFeature;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Difficulty;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -155,10 +148,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -169,8 +160,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
@@ -178,14 +167,21 @@ public class NMSBinding implements INMSBinding {
|
||||
private volatile DataVersion dataVersion;
|
||||
private final BlockData AIR = Material.AIR.createBlockData();
|
||||
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle();
|
||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
|
||||
|
||||
@Override
|
||||
public PlatformGenerationRegistry generationRegistry() {
|
||||
return new NmsGenerationRegistry(
|
||||
this::registry,
|
||||
"bukkit-generation-registry-v1",
|
||||
"bukkit-v26_2_R1-generated-registry-json-v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
@@ -824,6 +820,11 @@ public class NMSBinding implements INMSBinding {
|
||||
return NativeStructureVolumeIndex.volumes(engine, minX, minZ, maxX, maxZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||
NativeStructureVolumeIndex.invalidate(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiomeId(Biome biome) {
|
||||
for (World i : Bukkit.getWorlds()) {
|
||||
@@ -1112,6 +1113,17 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(World world) {
|
||||
return NmsSavedTerrainCapture.flush(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(World world, int chunkX, int chunkZ,
|
||||
int minimumY, int height) {
|
||||
return NmsSavedTerrainCapture.capture(world, chunkX, chunkZ, minimumY, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveAndUnloadChunk(World world, int x, int z) {
|
||||
try {
|
||||
@@ -1131,13 +1143,26 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushChunkIO(World world) {
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
MoonriseRegionFileIO.flush(level);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
public boolean pollChunkTask(World world) {
|
||||
if (FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())) {
|
||||
return false;
|
||||
}
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
if (Thread.currentThread() != level.getServer().getRunningThread()) {
|
||||
throw new IllegalStateException("Native chunk tasks require the server lifecycle thread");
|
||||
}
|
||||
return level.getChunkSource().pollTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushChunkIO(World world) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
MoonriseRegionFileIO.flush(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconcileNativeStructurePois(Chunk chunk) {
|
||||
NativeStructurePoiUpdates.reconcile((LevelChunk) ((CraftChunk) chunk).getHandle(ChunkStatus.FULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1260,9 +1285,9 @@ public class NMSBinding implements INMSBinding {
|
||||
: null;
|
||||
boolean studioBootstrap = platformGenerator != null
|
||||
&& platformGenerator.isStudioEntryBootstrapActive();
|
||||
boolean jigsawStudio = platformGenerator != null
|
||||
&& platformGenerator.isJigsawStudioActive();
|
||||
if (jigsawStudio) {
|
||||
boolean authoringStudio = platformGenerator != null
|
||||
&& platformGenerator.isAuthoringStudio();
|
||||
if (authoringStudio) {
|
||||
requireIrisGenerator(generator);
|
||||
if (currentState.possibleStructureSets().isEmpty()) {
|
||||
currentState.ensureStructuresGenerated();
|
||||
@@ -1415,7 +1440,14 @@ public class NMSBinding implements INMSBinding {
|
||||
if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) {
|
||||
return null;
|
||||
}
|
||||
return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason);
|
||||
CraftWorld world = (CraftWorld) location.getWorld();
|
||||
if (world.getDifficulty() == Difficulty.PEACEFUL) {
|
||||
EntityType<?> nativeType = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString()));
|
||||
if (nativeType == null || !nativeType.isAllowedInPeaceful()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return world.spawn(location, type.getEntityClass(), null, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1533,113 +1565,37 @@ public class NMSBinding implements INMSBinding {
|
||||
|
||||
@Override
|
||||
public boolean injectBukkit() {
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArguments(4))
|
||||
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||
.and(ElementMatchers.takesArgument(1, String.class))
|
||||
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
serverLevelTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return worldLifecycle.injectBukkit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureServerLevelInjection() {
|
||||
if (!injected.get())
|
||||
return;
|
||||
try {
|
||||
Agent.getInstrumentation().retransformClasses(
|
||||
LevelStorageSource.LevelStorageAccess.class,
|
||||
ServerLevel.class
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to re-apply Bukkit world lifecycle injection");
|
||||
}
|
||||
worldLifecycle.ensureServerLevelInjection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninjectBukkit() {
|
||||
synchronized (injected) {
|
||||
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
injected.set(false);
|
||||
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||
activeServerLevel,
|
||||
activeStorageAccess
|
||||
}) {
|
||||
if (transformer == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
worldLifecycle.uninjectBukkit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
|
||||
return ServerShutdownBoundary.await(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread(),
|
||||
timeout,
|
||||
unit
|
||||
);
|
||||
public boolean isServerStopping() {
|
||||
return worldLifecycle.isServerStopping();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
return worldLifecycle.createServerShutdownBoundary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deferPluginClassLoaderClose() {
|
||||
worldLifecycle.deferPluginClassLoaderClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePluginClassLoaderClose() {
|
||||
worldLifecycle.releasePluginClassLoaderClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1685,187 +1641,4 @@ public class NMSBinding implements INMSBinding {
|
||||
settings.updateLayers();
|
||||
return new FlatLevelSource(settings);
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LevelStorageAccessAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(1) String levelId,
|
||||
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||
) {
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator;
|
||||
try {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (generator == null || !generatorType.isInstance(generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.AllArguments Object[] constructorArguments
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
|
||||
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||
String levelId;
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
levelId = dimensionKey.identifier().getPath();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||
return;
|
||||
}
|
||||
|
||||
// From here Iris is present and its classes resolve; a failure resolving the
|
||||
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||
// the vanilla stem corrupts generation.
|
||||
ChunkGenerator gen = null;
|
||||
try {
|
||||
ChunkGenerator constructorGenerator = null;
|
||||
for (Object argument : constructorArguments) {
|
||||
if (argument instanceof ChunkGenerator candidate) {
|
||||
constructorGenerator = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
gen = owned;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||
try {
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
java.lang.reflect.Method stemMethod = null;
|
||||
for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
final class NativeStructurePoiUpdates {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
private final int skipPoiFlag;
|
||||
|
||||
NativeStructurePoiUpdates(int skipPoiFlag) {
|
||||
this.skipPoiFlag = skipPoiFlag;
|
||||
}
|
||||
|
||||
static NativeStructurePoiUpdates create() {
|
||||
try {
|
||||
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
|
||||
} catch (NoSuchFieldException e) {
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
|
||||
if ((flags & skipPoiFlag) != 0) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
ChunkAccess chunk = world.getChunk(position);
|
||||
if (!(chunk instanceof ProtoChunk)) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
if (!(chunk instanceof ImposterProtoChunk)
|
||||
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
|
||||
markSection(chunk, chunk.getSectionIndex(position.getY()));
|
||||
}
|
||||
return world.setBlock(position, state, flags | skipPoiFlag, limit);
|
||||
}
|
||||
|
||||
static void reconcile(LevelChunk chunk) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
if (sections == null) {
|
||||
return;
|
||||
}
|
||||
validateSections(chunk, sections);
|
||||
ServerLevel world = (ServerLevel) chunk.getLevel();
|
||||
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
|
||||
PoiManager manager = world.getPoiManager();
|
||||
for (int word = 0; word < sections.length; word++) {
|
||||
long remaining = sections[word];
|
||||
while (remaining != 0) {
|
||||
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
|
||||
reconcileSection(chunk, manager, index);
|
||||
remaining &= remaining - 1;
|
||||
}
|
||||
}
|
||||
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
|
||||
chunk.markUnsaved();
|
||||
}
|
||||
|
||||
private static void markSection(ChunkAccess chunk, int index) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
int word = index >>> 6;
|
||||
long bit = 1L << (index & 63);
|
||||
if (sections == null) {
|
||||
sections = new long[word + 1];
|
||||
} else {
|
||||
validateSections(chunk, sections);
|
||||
if (word < sections.length && (sections[word] & bit) != 0) {
|
||||
return;
|
||||
}
|
||||
if (word >= sections.length) {
|
||||
sections = Arrays.copyOf(sections, word + 1);
|
||||
}
|
||||
}
|
||||
sections[word] |= bit;
|
||||
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
|
||||
}
|
||||
|
||||
private static void validateSections(ChunkAccess chunk, long[] sections) {
|
||||
int count = chunk.getSectionsCount();
|
||||
int words = (count + 63) >>> 6;
|
||||
if (sections.length > words
|
||||
|| sections.length == words && (count & 63) != 0
|
||||
&& (sections[words - 1] >>> (count & 63)) != 0) {
|
||||
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
|
||||
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
|
||||
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
|
||||
if (existing.isPresent()) {
|
||||
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
|
||||
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
|
||||
manager.remove(record.getPos());
|
||||
}
|
||||
}
|
||||
}
|
||||
LevelChunkSection section = chunk.getSection(index);
|
||||
if (!section.maybeHas(PoiTypes::hasPoi)) {
|
||||
return;
|
||||
}
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
|
||||
if (type.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
|
||||
sectionPosition.minBlockZ() + z);
|
||||
if (!manager.getType(position).equals(type)) {
|
||||
manager.add(position.immutable(), type.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-11
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
@@ -11,6 +11,7 @@ import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Mth;
|
||||
@@ -59,11 +60,14 @@ import java.util.function.Supplier;
|
||||
|
||||
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private static final int WRITE_RADIUS = 1;
|
||||
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
|
||||
|
||||
private final WorldGenLevel delegate;
|
||||
private final ChunkPos generationCenter;
|
||||
private final IntBinaryOperator surfaceFirstFreeY;
|
||||
private final IntBinaryOperator floorFirstFreeY;
|
||||
private final boolean virtualizeHeights;
|
||||
private final Predicate<BlockPos> protectedPosition;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final BiomeManager biomeManager;
|
||||
private final AABB generationBounds;
|
||||
@@ -80,6 +84,9 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||
this.floorFirstFreeY = Objects.requireNonNull(
|
||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||
this.protectedPosition = Objects.requireNonNull(
|
||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||
this.fallbackBiome = delegate.getBiome(biomeSample);
|
||||
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
|
||||
@@ -99,9 +106,11 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY));
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -246,7 +255,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
||||
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
return delegate.getHeight(type, x, z);
|
||||
}
|
||||
return height(type, x, z);
|
||||
@@ -359,13 +368,13 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public BlockEntity getBlockEntity(BlockPos position) {
|
||||
return isReadable(position) ? delegate.getBlockEntity(position) : null;
|
||||
return isWritable(position) ? delegate.getBlockEntity(position) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> Optional<T> getBlockEntity(
|
||||
BlockPos position, BlockEntityType<T> type) {
|
||||
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
|
||||
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -424,17 +433,31 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
|
||||
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
|
||||
return isWritable(position)
|
||||
&& (POI_UPDATES != null
|
||||
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
|
||||
: delegate.setBlock(position, state, updateFlags, updateLimit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeBlock(BlockPos position, boolean move) {
|
||||
return isWritable(position) && delegate.removeBlock(position, move);
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
|
||||
: delegate.removeBlock(position, move);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
|
||||
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? !delegate.getBlockState(position).isAir()
|
||||
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
|
||||
: delegate.destroyBlock(position, drop, source, updateLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -453,7 +476,7 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
}
|
||||
|
||||
private boolean isWritable(BlockPos position) {
|
||||
return isReadable(position);
|
||||
return isReadable(position) && !protectedPosition.test(position);
|
||||
}
|
||||
|
||||
private boolean isWritableNeighbourhood(BlockPos position) {
|
||||
@@ -544,7 +567,9 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
private record Boundary(ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
}
|
||||
|
||||
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import java.util.function.LongPredicate;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class NmsGenerationHooks {
|
||||
private NmsGenerationHooks() {
|
||||
}
|
||||
|
||||
static void install() {
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
buddy.redefine(WorldGenRegion.class)
|
||||
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
|
||||
.and(ElementMatchers.takesArguments(BlockPos.class))))
|
||||
.make()
|
||||
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
private static class WorldGenerationWriteAdvice {
|
||||
@Advice.OnMethodExit
|
||||
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
|
||||
@Advice.Return(readOnly = false) boolean allowed) {
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
|
||||
if (generator instanceof LongPredicate predicate
|
||||
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator")
|
||||
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
|
||||
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
|
||||
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
|
||||
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
|
||||
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
|
||||
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
|
||||
private static final String BLOCK_REGISTRY = "minecraft:block";
|
||||
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
|
||||
|
||||
private final Supplier<RegistryAccess> registryAccess;
|
||||
private final String runtimeIdentity;
|
||||
private final String rendererIdentity;
|
||||
|
||||
NmsGenerationRegistry(
|
||||
Supplier<RegistryAccess> registryAccess,
|
||||
String runtimeIdentity,
|
||||
String rendererIdentity
|
||||
) {
|
||||
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
|
||||
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
|
||||
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String runtimeIdentity() {
|
||||
return runtimeIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generatedDefinitionRendererIdentity() {
|
||||
return rendererIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String customBiomeResourceKey(String identitySha256) {
|
||||
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> legacyCustomBiomeResourceKeys(
|
||||
String packName,
|
||||
String dimensionKey,
|
||||
String customBiomeId
|
||||
) {
|
||||
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
|
||||
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
|
||||
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition definition(String registryKey, String resourceKey) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
|
||||
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
|
||||
case DIMENSION_TYPE_REGISTRY -> presence(
|
||||
access,
|
||||
Registries.DIMENSION_TYPE,
|
||||
registryKey,
|
||||
resourceKey
|
||||
);
|
||||
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
|
||||
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
|
||||
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
|
||||
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
|
||||
default -> throw new IllegalArgumentException(
|
||||
"Registry does not accept generated JSON definitions: " + registryKey
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition generatedDefinition(String registryKey, String resourceKey) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> exactRegistered(
|
||||
access,
|
||||
Registries.BIOME,
|
||||
Biome.DIRECT_CODEC,
|
||||
resourceKey
|
||||
);
|
||||
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
|
||||
access,
|
||||
Registries.DIMENSION_TYPE,
|
||||
DimensionType.DIRECT_CODEC,
|
||||
resourceKey
|
||||
);
|
||||
default -> throw new IllegalArgumentException(
|
||||
"Registry does not contain Iris-generated definitions: " + registryKey
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static <T> Definition presence(
|
||||
RegistryAccess access,
|
||||
ResourceKey<? extends Registry<T>> registryKey,
|
||||
String registryResourceKey,
|
||||
String resourceKey
|
||||
) {
|
||||
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||
Identifier identifier = requireIdentifier(resourceKey);
|
||||
T value = registry.getValue(identifier);
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
|
||||
+ " / " + resourceKey + ".");
|
||||
}
|
||||
return Definition.resourceIdentity(registryResourceKey, resourceKey);
|
||||
}
|
||||
|
||||
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
|
||||
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
|
||||
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
|
||||
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
|
||||
return Definition.exactJson(encoded.toString());
|
||||
}
|
||||
|
||||
private static <T> Definition exactRegistered(
|
||||
RegistryAccess access,
|
||||
ResourceKey<? extends Registry<T>> registryKey,
|
||||
Codec<T> codec,
|
||||
String resourceKey
|
||||
) {
|
||||
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||
Identifier identifier = requireIdentifier(resourceKey);
|
||||
T value = registry.getValue(identifier);
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Missing generated registry definition "
|
||||
+ registryKey.identifier() + " / " + resourceKey + ".");
|
||||
}
|
||||
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
|
||||
return Definition.exactJson(encoded.toString());
|
||||
}
|
||||
|
||||
private static Identifier requireIdentifier(String resourceKey) {
|
||||
Identifier identifier = Identifier.tryParse(resourceKey);
|
||||
if (identifier == null) {
|
||||
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static String requireText(String value, String label) {
|
||||
String required = Objects.requireNonNull(value, label).trim();
|
||||
if (required.isEmpty()) {
|
||||
throw new IllegalArgumentException(label + " must not be blank.");
|
||||
}
|
||||
return required;
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
final class NmsSavedTerrainCapture {
|
||||
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
|
||||
private static final int SNAPSHOT_BATCH_SIZE = 64;
|
||||
|
||||
private NmsSavedTerrainCapture() {
|
||||
}
|
||||
|
||||
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||
J.runRegionFuture(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
|
||||
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||
if (chunk == null) {
|
||||
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
|
||||
return;
|
||||
}
|
||||
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
|
||||
if (!SavedTerrainChunk.hasTerrain(status)) {
|
||||
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
|
||||
}
|
||||
snapshot.complete(scheduleSnapshot(level, chunk));
|
||||
} catch (Throwable failure) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
return snapshot.thenApplyAsync(captured -> {
|
||||
try {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
|
||||
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||
if (data == null) {
|
||||
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
||||
}
|
||||
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
NbtIo.write(data, output);
|
||||
}
|
||||
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static CompletableFuture<Void> flush(World world) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
flushSnapshots(world, level, holders);
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
|
||||
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
|
||||
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
|
||||
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
|
||||
awaitSnapshots(snapshots, deadline);
|
||||
for (CompletableFuture<CapturedChunk> future : snapshots) {
|
||||
CapturedChunk captured = future.join();
|
||||
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
|
||||
}
|
||||
for (int index = offset; index < end; index++) {
|
||||
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
|
||||
}
|
||||
}
|
||||
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
|
||||
}
|
||||
|
||||
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
|
||||
World world, ServerLevel level, List<NewChunkHolder> holders) {
|
||||
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
|
||||
for (NewChunkHolder original : holders) {
|
||||
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
|
||||
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||
snapshots.add(snapshot);
|
||||
J.runRegionFuture(world, position.x(), position.z(), () -> {
|
||||
try {
|
||||
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
|
||||
.getChunkHolder(position.x(), position.z());
|
||||
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
|
||||
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
|
||||
} catch (Throwable failure) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
|
||||
throws IOException {
|
||||
try {
|
||||
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
|
||||
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
for (Checkpoint checkpoint : checkpoints) {
|
||||
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||
}
|
||||
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
for (Checkpoint checkpoint : checkpoints) {
|
||||
verifyCheckpoint(worldFolder, checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
|
||||
if (data == null) {
|
||||
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
|
||||
}
|
||||
byte[] receipt = data.getCompound("ChunkBukkitValues")
|
||||
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
|
||||
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
|
||||
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
|
||||
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
|
||||
data.getStringOr("Status", ""), receipt, structureActivation));
|
||||
}
|
||||
|
||||
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
|
||||
Verification expected = checkpoint.verification();
|
||||
if (expected == null) {
|
||||
return;
|
||||
}
|
||||
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
|
||||
expected.status(), expected.receipt(), expected.structureActivation());
|
||||
}
|
||||
|
||||
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||
while (true) {
|
||||
checkWriteWait(chunk, deadlineNanos);
|
||||
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
|
||||
return;
|
||||
}
|
||||
helpSaveTasks(scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
|
||||
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
||||
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
||||
Completable<CompoundTag> nativeData = new Completable<>();
|
||||
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
|
||||
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
|
||||
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
|
||||
task.queue();
|
||||
ChunkPos position = chunk.getPos();
|
||||
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
|
||||
return new CapturedChunk(position, data);
|
||||
}
|
||||
|
||||
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
|
||||
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||
while (!captured.data().isDone()) {
|
||||
checkWriteWait(captured.chunk(), deadlineNanos);
|
||||
helpSaveTasks(scheduler);
|
||||
}
|
||||
try {
|
||||
return captured.data().join();
|
||||
} catch (CompletionException failure) {
|
||||
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
|
||||
CompletableFuture<CompoundTag> data) {
|
||||
try {
|
||||
CompoundTag serialized = snapshot.write();
|
||||
nativeData.complete(serialized);
|
||||
data.complete(serialized);
|
||||
} catch (Throwable failure) {
|
||||
nativeData.completeExceptionally(failure);
|
||||
data.completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
|
||||
}
|
||||
if (System.nanoTime() - deadlineNanos >= 0L) {
|
||||
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
|
||||
boolean helped = scheduler.saveExecutor.executeTask();
|
||||
helped |= scheduler.compressionExecutor.executeTask();
|
||||
if (!helped) {
|
||||
LockSupport.parkNanos(1_000_000L);
|
||||
}
|
||||
}
|
||||
|
||||
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
|
||||
}
|
||||
|
||||
record Checkpoint(ChunkPos chunk, Verification verification) {
|
||||
}
|
||||
|
||||
record Verification(String status, byte[] receipt, long structureActivation) {
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.bytebuddy.utility.JavaModule;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
final class NmsWorldLifecycle {
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||
private boolean pluginClassLoaderCloseDeferred;
|
||||
|
||||
public boolean injectBukkit() {
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
Agent.requireClassLoaderCloseDeferral();
|
||||
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||
.getMethod("close").getDeclaringClass();
|
||||
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||
.with(loaderListener)
|
||||
.type(ElementMatchers.is(loaderCloseType))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||
.on(ElementMatchers.named("close")
|
||||
.and(ElementMatchers.takesArguments(0))
|
||||
.and(ElementMatchers.returns(void.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
loaderListener.requireInstalled();
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArguments(4))
|
||||
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||
.and(ElementMatchers.takesArgument(1, String.class))
|
||||
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
serverLevelTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
NmsGenerationHooks.install();
|
||||
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
pluginClassLoaderTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess,
|
||||
partialPluginClassLoader
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable cleanupFailure) {
|
||||
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureServerLevelInjection() {
|
||||
if (!injected.get()) {
|
||||
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
|
||||
}
|
||||
try {
|
||||
Agent.getInstrumentation().retransformClasses(
|
||||
LevelStorageSource.LevelStorageAccess.class,
|
||||
ServerLevel.class
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void uninjectBukkit() {
|
||||
synchronized (injected) {
|
||||
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
injected.set(false);
|
||||
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||
activeServerLevel,
|
||||
activeStorageAccess
|
||||
}) {
|
||||
if (transformer == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isServerStopping() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||
}
|
||||
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||
return new ServerShutdownBoundary(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread()
|
||||
);
|
||||
}
|
||||
|
||||
public void deferPluginClassLoaderClose() {
|
||||
synchronized (injected) {
|
||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||
return;
|
||||
}
|
||||
Agent.retainClassLoader(getClass().getClassLoader());
|
||||
pluginClassLoaderCloseDeferred = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void releasePluginClassLoaderClose() {
|
||||
ResettableClassFileTransformer transformer;
|
||||
boolean releaseLoader;
|
||||
synchronized (injected) {
|
||||
transformer = pluginClassLoaderTransformer;
|
||||
pluginClassLoaderTransformer = null;
|
||||
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||
pluginClassLoaderCloseDeferred = false;
|
||||
}
|
||||
try {
|
||||
if (transformer != null
|
||||
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||
}
|
||||
} finally {
|
||||
if (releaseLoader) {
|
||||
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||
private volatile boolean transformed;
|
||||
private volatile Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, DynamicType dynamicType) {
|
||||
transformed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, Throwable throwable) {
|
||||
failure = throwable;
|
||||
}
|
||||
|
||||
private void requireInstalled() {
|
||||
if (!transformed || failure != null) {
|
||||
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginClassLoaderCloseAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||
Class<?> installer = Class.forName(
|
||||
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||
.invoke(null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
private static class LevelStorageAccessAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(1) String levelId,
|
||||
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||
) {
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator;
|
||||
try {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (generator == null || !generatorType.isInstance(generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.AllArguments Object[] constructorArguments
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
|
||||
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||
String levelId;
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
levelId = dimensionKey.identifier().getPath();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||
return;
|
||||
}
|
||||
|
||||
// From here Iris is present and its classes resolve; a failure resolving the
|
||||
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||
// the vanilla stem corrupts generation.
|
||||
ChunkGenerator gen = null;
|
||||
try {
|
||||
ChunkGenerator constructorGenerator = null;
|
||||
for (Object argument : constructorArguments) {
|
||||
if (argument instanceof ChunkGenerator candidate) {
|
||||
constructorGenerator = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
gen = owned;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||
try {
|
||||
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
Method stemMethod = null;
|
||||
for (Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
|
||||
import net.minecraft.core.Holder;
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CustomBiomeSourceStructureContractTest {
|
||||
@Test
|
||||
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
|
||||
assertTrue(source.contains("resolveBiomeHolder(registry, i.getStructureDerivativeKey())"));
|
||||
assertTrue(source.contains("resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey())"));
|
||||
assertTrue(source.contains("resolution.irisBiome.getStructureDerivativeKey()"));
|
||||
assertFalse(source.contains("resolution.irisBiome.getVanillaDerivative()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeRuntimeReadsAreGenerationLeased() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_possible_biomes\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||
assertTrue(source.contains("if (engine.isClosed())"));
|
||||
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
|
||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||
assertTrue(source.contains("e.isExpectedTeardown()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void strongholdRingSearchSamplesOneQuartColumnPerChunk() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0"));
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112"));
|
||||
assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4"));
|
||||
assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler"));
|
||||
assertTrue(source.contains(
|
||||
"return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleBiomeBatchReusesTheCallersGenerationLease() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||
int lookupStart = source.indexOf("public Holder<Biome> getVisibleNoiseBiome(");
|
||||
int lookupEnd = source.indexOf("private GenerationSessionLease tryAcquireGenerationLease(", lookupStart);
|
||||
|
||||
assertTrue(lookupStart >= 0);
|
||||
assertTrue(lookupEnd > lookupStart);
|
||||
String lookup = source.substring(lookupStart, lookupEnd);
|
||||
|
||||
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
|
||||
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
|
||||
< lookup.indexOf("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||
assertFalse(source.contains("studioBootstrapNoiseBiomeCache"));
|
||||
}
|
||||
}
|
||||
-551
@@ -1,551 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisChunkGeneratorFailureContractTest {
|
||||
@Test
|
||||
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
|
||||
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
|
||||
String locate = source.substring(locateStart, locateEnd);
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
|
||||
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
|
||||
String reachability = source.substring(reachabilityStart, reachabilityEnd);
|
||||
int bootstrapGate = locate.indexOf("!platformGenerator.shouldGenerateStructures()");
|
||||
int generatorIdentity = locate.indexOf("level.getChunkSource().getGenerator() != this");
|
||||
int lease = locate.indexOf("requireGenerationLease(\"bukkit_nms_structure_locate\")");
|
||||
int nativePrediction = locate.indexOf("NativeStructureVanillaLocator.predict(");
|
||||
|
||||
assertTrue(locate.contains("reached its safety limit"));
|
||||
assertTrue(locate.contains("unregistered structure holder"));
|
||||
assertTrue(bootstrapGate >= 0);
|
||||
assertTrue(generatorIdentity > bootstrapGate);
|
||||
assertTrue(lease > generatorIdentity);
|
||||
assertTrue(nativePrediction > lease);
|
||||
assertFalse(locate.contains("catch (Throwable"));
|
||||
assertFalse(locate.contains("IrisLogging.reportError"));
|
||||
assertFalse(reachability.contains("catch (Throwable"));
|
||||
assertFalse(reachability.contains("reachable = Set.of()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
|
||||
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
|
||||
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
|
||||
assertTrue(adjustment.contains("\"vertical adjustment\""));
|
||||
assertFalse(adjustment.contains("IrisLogging.reportError"));
|
||||
assertTrue(placement.contains("\"resolution\""));
|
||||
assertTrue(placement.contains("\"terrain integration\""));
|
||||
assertTrue(placement.contains("\"terrain preparation\""));
|
||||
assertTrue(placement.contains("\"foundation repair\""));
|
||||
assertFalse(placement.contains("\"terrain carving\""));
|
||||
assertTrue(placement.contains("\"vegetation cleanup\""));
|
||||
assertTrue(placement.contains("\"placement\""));
|
||||
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
|
||||
assertTrue(placement.contains("prepareSurfaceStructures"));
|
||||
assertTrue(placement.contains("clearIntersectingVegetation"));
|
||||
assertTrue(placement.indexOf("clearIntersectingVegetation")
|
||||
< placement.indexOf("prepareSurfaceStructures"));
|
||||
assertTrue(placement.indexOf("prepareSurfaceStructures")
|
||||
< placement.indexOf("for (NativePlacementGroup group"));
|
||||
assertTrue(placement.indexOf("repairVacuumFoundations")
|
||||
> placement.indexOf("for (NativePlacementGroup group"));
|
||||
assertFalse(placement.contains("IrisLogging.reportError"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heightmapRuntimeReadsAreGenerationLeased() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
|
||||
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_heightmaps\")"));
|
||||
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_height\")"));
|
||||
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_base_column\")"));
|
||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureReferenceRepairIsGenerationLeased() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int referencesStart = source.indexOf("public void createReferences");
|
||||
int referencesEnd = source.indexOf("public CompletableFuture<ChunkAccess> createBiomes", referencesStart);
|
||||
String references = source.substring(referencesStart, referencesEnd);
|
||||
|
||||
int nativeStructureGate = references.indexOf("!platformGenerator.shouldGenerateStructures()");
|
||||
int generatorIdentity = references.indexOf("runtimeLevel.getChunkSource().getGenerator() != this");
|
||||
int stage = references.indexOf("requireGenerationStage(\"bukkit_nms_create_references\")");
|
||||
int lease = references.indexOf("requireGenerationLease(\"bukkit_nms_create_references\")");
|
||||
int repair = references.indexOf("NativeStructureReferenceRepair.createReferences(");
|
||||
|
||||
assertTrue(nativeStructureGate >= 0);
|
||||
assertTrue(generatorIdentity > nativeStructureGate);
|
||||
assertTrue(stage > generatorIdentity);
|
||||
assertTrue(lease > stage);
|
||||
assertTrue(repair > lease);
|
||||
assertTrue(references.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||
assertFalse(references.contains("delegate.createReferences("));
|
||||
assertFalse(references.contains("catch ("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureGenerationAcquiresBootstrapGateBeforeUsingPublishedState() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int generationStart = source.indexOf("public void createStructures(");
|
||||
int generationEnd = source.indexOf("private void adjustGeneratedStructures", generationStart);
|
||||
String generation = source.substring(generationStart, generationEnd);
|
||||
int gate = generation.indexOf("!platformGenerator.shouldGenerateStructures()");
|
||||
int currentGenerator = generation.indexOf(
|
||||
"runtimeLevel.getChunkSource().getGenerator() != this");
|
||||
int stateIdentity = generation.indexOf(
|
||||
"runtimeLevel.getChunkSource().getGeneratorState() != structureState");
|
||||
int stage = generation.indexOf(
|
||||
"requireGenerationStage(\"bukkit_nms_create_structures\")");
|
||||
int lease = generation.indexOf(
|
||||
"requireGenerationLease(\"bukkit_nms_create_structures\")");
|
||||
int generationCall = generation.indexOf(
|
||||
"super.createStructures(registryAccess, structureState");
|
||||
|
||||
assertTrue(gate >= 0);
|
||||
assertTrue(currentGenerator > gate);
|
||||
assertTrue(stateIdentity > currentGenerator);
|
||||
assertTrue(stage > stateIdentity);
|
||||
assertTrue(lease > stage);
|
||||
assertTrue(generationCall > lease);
|
||||
assertTrue(generation.contains("structureState,\n structureManager"));
|
||||
assertFalse(generation.contains("resolvePublishedStructureState"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeRingBootstrapTracksTheExactMinecraftFutures() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int initializeStart = source.indexOf("private CompletableFuture<Void> initializeStructureState(");
|
||||
int initializeEnd = source.indexOf("private Map<?, ?> structureRingPositions", initializeStart);
|
||||
String initialize = source.substring(initializeStart, initializeEnd);
|
||||
int completionStart = source.indexOf(
|
||||
"private CompletableFuture<Void> structureRingCompletion", initializeEnd);
|
||||
int completionEnd = source.indexOf("private void requireCurrentStructureOwner", completionStart);
|
||||
String completion = source.substring(completionStart, completionEnd);
|
||||
|
||||
assertBefore(initialize,
|
||||
"structureRingPositions(structureState)",
|
||||
"structureState.ensureStructuresGenerated()");
|
||||
assertBefore(initialize,
|
||||
"structureState.ensureStructuresGenerated()",
|
||||
"structureRingCompletion(ringPositions)");
|
||||
assertFalse(initialize.contains("catch ("));
|
||||
assertTrue(completion.contains("for (Object candidate : ringPositions.values())"));
|
||||
assertTrue(completion.contains("CompletableFuture.allOf(completions)"));
|
||||
assertTrue(completion.contains("throw new IllegalStateException("));
|
||||
assertFalse(completion.contains("join()"));
|
||||
assertFalse(completion.contains("get()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardActivationClaimsTheExactPublishedStateBeforeInitialization() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int activationStart = source.indexOf("CompletableFuture<Void> activateStudioStructureState(");
|
||||
int activationEnd = source.indexOf(
|
||||
"private CompletableFuture<Void> startStructureStateBootstrap", activationStart);
|
||||
String activation = source.substring(activationStart, activationEnd);
|
||||
int bootstrapStart = activationEnd;
|
||||
int bootstrapEnd = source.indexOf(
|
||||
"private CompletableFuture<Void> initializeStructureState", bootstrapStart);
|
||||
String bootstrap = source.substring(bootstrapStart, bootstrapEnd);
|
||||
|
||||
assertTrue(activation.contains("retained.structureState()"));
|
||||
assertBefore(activation,
|
||||
"retainedStudioStructureState(",
|
||||
"claimStudioStructureState(retained)");
|
||||
assertBefore(bootstrap,
|
||||
"claim,",
|
||||
"initializeStructureState(");
|
||||
assertBefore(bootstrap,
|
||||
"initializeStructureState(",
|
||||
"activation.run()");
|
||||
assertFalse(activation.contains("publishStructureState("));
|
||||
assertFalse(source.contains("ChunkGeneratorStructureState fullState"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainWritesPrimeTheWorldgenHeightmapsForEveryChunk() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||
int fillEnd = source.indexOf("public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt", fillStart);
|
||||
String fill = source.substring(fillStart, fillEnd);
|
||||
int decorationStart = source.indexOf("public void addVanillaDecorations");
|
||||
int decorationEnd = source.indexOf("public void spawnOriginalMobs", decorationStart);
|
||||
String decorations = source.substring(decorationStart, decorationEnd);
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
|
||||
assertTrue(fill.contains("thenApply"));
|
||||
assertTrue(fill.contains("primeWorldgenHeightmaps"));
|
||||
assertTrue(decorations.contains("primeWorldgenHeightmaps"));
|
||||
assertFalse(decorations.contains("Heightmap.Types.WORLD_SURFACE_WG"));
|
||||
assertFalse(decorations.contains("Heightmap.Types.OCEAN_FLOOR_WG"));
|
||||
assertEquals(1, occurrences(source, "WorldgenTerrainHeightmaps.primeTerrain("));
|
||||
assertTrue(placement.contains("WorldgenTerrainHeightmaps.primeStructurePlacement("));
|
||||
assertTrue(placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement(")
|
||||
< placement.indexOf("prepareSurfaceStructures"));
|
||||
assertTrue(fill.contains("requireGenerationLease(\"bukkit_nms_chunk_pipeline\")"));
|
||||
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
|
||||
.replace("\r\n", "\n");
|
||||
String createBiomes = method(
|
||||
source,
|
||||
"public CompletableFuture<ChunkAccess> createBiomes",
|
||||
"public void buildSurface");
|
||||
String buildSurface = method(
|
||||
source,
|
||||
"public void buildSurface",
|
||||
"public void applyCarvers");
|
||||
String carvers = method(
|
||||
source,
|
||||
"public void applyCarvers",
|
||||
"public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||
String noise = method(
|
||||
source,
|
||||
"public CompletableFuture<ChunkAccess> fillFromNoise",
|
||||
"private static boolean isCancellationFailure");
|
||||
String decoration = method(
|
||||
source,
|
||||
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
||||
"public BiomeGenerationSettings getBiomeGenerationSettings");
|
||||
String spawning = method(
|
||||
source,
|
||||
"public void spawnOriginalMobs",
|
||||
"private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables");
|
||||
String baseHeight = method(
|
||||
source,
|
||||
"public int getBaseHeight",
|
||||
"public NoiseColumn getBaseColumn");
|
||||
String baseColumn = method(
|
||||
source,
|
||||
"public NoiseColumn getBaseColumn",
|
||||
"private GenerationSessionLease requireGenerationLease");
|
||||
|
||||
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
|
||||
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
|
||||
assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
|
||||
assertTrue(createBiomes.contains("sampler, resolverState"));
|
||||
assertTrue(buildSurface.contains("delegate.buildSurface("));
|
||||
assertTrue(carvers.contains("delegate.applyCarvers("));
|
||||
assertTrue(noise.contains("requireNoiseGenerationStage("));
|
||||
assertTrue(noise.contains("\"bukkit_nms_chunk_pipeline\""));
|
||||
assertTrue(noise.contains("stage.close()"));
|
||||
assertTrue(noise.contains("completion.cancel(false)"));
|
||||
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
|
||||
assertTrue(spawning.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||
assertTrue(baseHeight.contains("engine.getHeight("));
|
||||
assertTrue(baseColumn.contains("engine.getHeight("));
|
||||
assertFalse(source.contains("synthetic_entry"));
|
||||
assertFalse(source.contains("StudioEntryChunkGenerator"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fillFromNoiseLeaseSpansTheDelegateAndHeightmapPipeline() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int fillStart = source.indexOf("public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||
int fillEnd = source.indexOf("private static boolean isCancellationFailure", fillStart);
|
||||
String fill = source.substring(fillStart, fillEnd);
|
||||
int productionStart = fill.indexOf(
|
||||
" BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
|
||||
0);
|
||||
assertTrue(productionStart >= 0);
|
||||
String productionFill = fill.substring(productionStart);
|
||||
int completionStart = productionFill.indexOf("pipeline.whenComplete(");
|
||||
int completionEnd = productionFill.indexOf(" return completion;", completionStart);
|
||||
assertTrue(completionStart >= 0);
|
||||
assertTrue(completionEnd > completionStart);
|
||||
String completion = productionFill.substring(completionStart, completionEnd);
|
||||
|
||||
assertBefore(productionFill,
|
||||
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
|
||||
"GenerationSessionLease lease");
|
||||
assertBefore(productionFill,
|
||||
"requireNoiseGenerationStage(",
|
||||
"\"bukkit_nms_chunk_pipeline\")");
|
||||
assertBefore(productionFill,
|
||||
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
|
||||
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
|
||||
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
|
||||
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
|
||||
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
|
||||
assertBefore(completion, "lease.close();", "stage.close();");
|
||||
assertBefore(completion, "stage.close();", "completion.complete(filled)");
|
||||
assertBefore(completion, "stage.close();", "completion.cancel(false)");
|
||||
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
|
||||
assertTrue(completion.contains("else if (cancelled)"));
|
||||
assertFalse(completion.contains("pipeline.isCancelled()"));
|
||||
assertFalse(completion.contains("finally"));
|
||||
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
|
||||
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
|
||||
assertTrue(productionFill.contains("return completion;"));
|
||||
assertFalse(productionFill.contains("return pipeline;"));
|
||||
assertFalse(productionFill.contains("pipeline.cancel("));
|
||||
assertFalse(productionFill.contains("bukkit_nms_worldgen_heightmaps"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
|
||||
Semaphore admission = new Semaphore(1, true);
|
||||
admission.acquire();
|
||||
CountDownLatch exclusiveEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseExclusive = new CountDownLatch(1);
|
||||
AtomicBoolean dependentFinished = new AtomicBoolean(false);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
boolean stageReleased = false;
|
||||
|
||||
try {
|
||||
Future<?> exclusive = executor.submit(() -> {
|
||||
boolean acquired = false;
|
||||
try {
|
||||
admission.acquire();
|
||||
acquired = true;
|
||||
exclusiveEntered.countDown();
|
||||
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
if (acquired) {
|
||||
admission.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
awaitQueueLength(admission, 1);
|
||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||
outward.thenRun(() -> {
|
||||
try {
|
||||
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
|
||||
dependentFinished.set(true);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
});
|
||||
|
||||
admission.release();
|
||||
stageReleased = true;
|
||||
assertTrue(outward.complete(null));
|
||||
assertTrue(dependentFinished.get());
|
||||
releaseExclusive.countDown();
|
||||
exclusive.get(2, TimeUnit.SECONDS);
|
||||
assertEquals(1, admission.availablePermits());
|
||||
} finally {
|
||||
if (!stageReleased) {
|
||||
admission.release();
|
||||
}
|
||||
releaseExclusive.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() throws IOException {
|
||||
CompletableFuture<Void> delegate = new CompletableFuture<>();
|
||||
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
|
||||
|
||||
assertTrue(delegate.cancel(false));
|
||||
assertFalse(transformed.isCancelled());
|
||||
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
String cancellation = method(
|
||||
source,
|
||||
"private static boolean isCancellationFailure",
|
||||
"private void primeWorldgenHeightmaps");
|
||||
assertTrue(cancellation.contains("current instanceof CompletionException"));
|
||||
assertTrue(cancellation.contains("current instanceof CancellationException"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noiseAdmissionCanPrepareStudioBeforeAcquiringTheGenerationLease() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
String fill = method(
|
||||
source,
|
||||
"public CompletableFuture<ChunkAccess> fillFromNoise",
|
||||
"private static boolean isCancellationFailure");
|
||||
String admission = method(
|
||||
source,
|
||||
"private BukkitChunkGenerator.GenerationStagePermit requireNoiseGenerationStage",
|
||||
"public Optional<Identifier> getTypeNameForDataFixer");
|
||||
|
||||
assertBefore(fill,
|
||||
"requireNoiseGenerationStage(",
|
||||
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
|
||||
assertTrue(admission.contains("platformGenerator.acquireNoiseGenerationStage("));
|
||||
assertBefore(admission, "engine,", "chunkPos.x(),");
|
||||
assertBefore(admission, "chunkPos.x(),", "chunkPos.z(),");
|
||||
assertFalse(fill.contains("requireGenerationStage(\"bukkit_nms_chunk_pipeline\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyTopLevelMoonriseStageEntersTheFairGateBeforeItsGenerationLease() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
|
||||
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
|
||||
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
|
||||
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
|
||||
String decoration = method(source,
|
||||
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
||||
"public BiomeGenerationSettings getBiomeGenerationSettings");
|
||||
|
||||
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
|
||||
assertStageBeforeLease(references, "bukkit_nms_create_references");
|
||||
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
|
||||
assertBefore(noise,
|
||||
"requireNoiseGenerationStage(",
|
||||
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
|
||||
assertStageBeforeLease(decoration, "bukkit_nms_biome_decoration");
|
||||
assertBefore(decoration,
|
||||
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
|
||||
"importedFeatures.prepare(generatoraccessseed)");
|
||||
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
|
||||
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
|
||||
assertFalse(source.contains("GenerationSessionManager"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException {
|
||||
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
|
||||
Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java");
|
||||
|
||||
assertTrue("Worldgen heightmap priming must be shared with the modded loaders through "
|
||||
+ nativegen, Files.isRegularFile(shared));
|
||||
|
||||
String heightmaps = Files.readString(shared).replace("\r\n", "\n");
|
||||
|
||||
assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;"));
|
||||
assertTrue(heightmaps.contains("public static void primeTerrain("));
|
||||
assertTrue(heightmaps.contains("public static void primeStructurePlacement("));
|
||||
assertFalse(heightmaps.contains("org.bukkit"));
|
||||
assertFalse(heightmaps.contains("craftbukkit"));
|
||||
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
|
||||
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlySidecarOwnedInjectedStartsUsePersistedPlacementPolicy() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int placementStart = source.indexOf("private void placeVanillaStructures");
|
||||
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
|
||||
String placement = source.substring(placementStart, placementEnd);
|
||||
int ownershipResolution = placement.indexOf(
|
||||
"NativeStructureOwnershipRecovery.resolve(");
|
||||
int persistedDecision = placement.indexOf("ownership.restoredDecision()", ownershipResolution);
|
||||
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
|
||||
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
|
||||
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
|
||||
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
|
||||
String factory = Files.readString(nativegen.resolve("NativeStructureFactory.java")).replace("\r\n", "\n");
|
||||
String injector = Files.readString(nativegen.resolve("NativeStructureStartInjector.java")).replace("\r\n", "\n");
|
||||
String recovery = Files.readString(nativegen.resolve("NativeStructureOwnershipRecovery.java")).replace("\r\n", "\n");
|
||||
int ownershipRecord = injector.indexOf("NativeStructureOwnershipStore.record(");
|
||||
int startPublication = injector.indexOf(
|
||||
"context.structureManager().setStartForStructure(", ownershipRecord);
|
||||
|
||||
assertTrue(ownershipResolution >= 0);
|
||||
assertTrue(persistedDecision > ownershipResolution);
|
||||
assertTrue(recovery.contains("NativeStructureOwnershipStore.findPersisted("));
|
||||
assertTrue(recovery.contains("NativeStructureOwnershipStore.record("));
|
||||
assertTrue(ownershipRecord >= 0);
|
||||
assertTrue(startPublication > ownershipRecord);
|
||||
assertTrue(factory.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
|
||||
assertTrue(adjustment.contains("NativeStructureReferenceEnvelope.wrapForPublication("));
|
||||
assertFalse(source.contains("wrapManaged("));
|
||||
assertFalse(source.contains("isIrisManagedStart("));
|
||||
}
|
||||
|
||||
private static int occurrences(String source, String needle) {
|
||||
int count = 0;
|
||||
int index = source.indexOf(needle);
|
||||
while (index >= 0) {
|
||||
count++;
|
||||
index = source.indexOf(needle, index + needle.length());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void awaitQueueLength(Semaphore semaphore, int expected) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
assertTrue("Expected at least " + expected + " queued semaphore threads",
|
||||
semaphore.getQueueLength() >= expected);
|
||||
}
|
||||
|
||||
private static String method(String source, String startToken, String endToken) {
|
||||
int start = source.indexOf(startToken);
|
||||
int end = source.indexOf(endToken, start);
|
||||
assertTrue("Missing source contract token: " + startToken, start >= 0);
|
||||
assertTrue("Missing source contract token: " + endToken, end > start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
|
||||
private static void assertStageBeforeLease(String source, String operation) {
|
||||
assertBefore(source,
|
||||
"requireGenerationStage(\"" + operation + "\")",
|
||||
"requireGenerationLease(\"" + operation + "\")");
|
||||
}
|
||||
|
||||
private static void assertBefore(String source, String first, String second) {
|
||||
int firstIndex = source.indexOf(first);
|
||||
int secondIndex = source.indexOf(second);
|
||||
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
|
||||
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
|
||||
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaChunkGenerationMobsUseTheVisibleBiomesVanillaDerivative() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int spawnStart = source.indexOf("public void spawnOriginalMobs");
|
||||
int spawnEnd = source.indexOf("private static WeightedList", spawnStart);
|
||||
String spawn = source.substring(spawnStart, spawnEnd);
|
||||
|
||||
assertTrue(spawn.contains("customBiomeSource.getVanillaSpawnBiome(visibleBiome)"));
|
||||
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
|
||||
assertTrue(spawn.contains("region.getBiome(center.getWorldPosition().atY(region.getMaxY()))"));
|
||||
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
|
||||
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));
|
||||
assertFalse(spawn.contains("delegate.spawnOriginalMobs"));
|
||||
}
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void nativeLocateAllowsMonumentsAfterPolicyAndReachabilityChecks() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
|
||||
assertTrue(findStart >= 0);
|
||||
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
|
||||
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
|
||||
assertTrue(irisHelperStart > findStart);
|
||||
assertTrue(filterStart > irisHelperStart);
|
||||
String outerMethod = source.substring(findStart, irisHelperStart);
|
||||
String irisHelper = source.substring(irisHelperStart, filterStart);
|
||||
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
|
||||
int placedCheck = irisHelper.indexOf(
|
||||
"if (!IrisStructureLocator.hasNativePlacement(engine, structureId))");
|
||||
int irisLocate = irisHelper.indexOf("NativeStructureLocatePersistence.search(", placedCheck);
|
||||
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
|
||||
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
|
||||
int nativePrediction = outerMethod.indexOf("NativeStructureVanillaLocator.predict(", nativeFilter);
|
||||
int irisResolution = outerMethod.indexOf("return findNearestIrisStructure(", nativePrediction);
|
||||
int nearestSelection = irisHelper.indexOf(
|
||||
"NativeStructureLocateResults.nearest(pos, predicted, nativeLocated)");
|
||||
int selectedVerification = irisHelper.indexOf("bestSearch.search().verify(bestResult)", nearestSelection);
|
||||
int selectedReference = irisHelper.indexOf(
|
||||
"selectedSearch.search().reference(selectedStart)", selectedVerification);
|
||||
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
|
||||
assertTrue(reachabilityStart > filterStart);
|
||||
String filterMethod = source.substring(filterStart, reachabilityStart);
|
||||
int policyFilter = filterMethod.indexOf("if (!decision.generate())");
|
||||
int filterContinue = filterMethod.indexOf("continue;", policyFilter);
|
||||
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", filterContinue);
|
||||
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
|
||||
|
||||
assertTrue(registryLookup >= 0);
|
||||
assertTrue(placedCheck > registryLookup);
|
||||
assertTrue(irisLocate > placedCheck);
|
||||
assertTrue(searchLimit > irisLocate);
|
||||
assertTrue(nativeFilter >= 0);
|
||||
assertTrue(nativePrediction > nativeFilter);
|
||||
assertTrue(irisResolution > nativePrediction);
|
||||
assertTrue(nearestSelection > irisLocate);
|
||||
assertTrue(selectedVerification > nearestSelection);
|
||||
assertTrue(selectedReference > selectedVerification);
|
||||
assertTrue(policyFilter >= 0);
|
||||
assertTrue(filterContinue > policyFilter);
|
||||
assertTrue(emptyNativePartition > filterContinue);
|
||||
assertTrue(reachabilityLookup > emptyNativePartition);
|
||||
assertFalse(filterMethod.contains("NativeStructureLocateCapability"));
|
||||
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
|
||||
assertTrue(irisHelper.contains("NativeStructureLocatePersistence.probe("));
|
||||
assertTrue(irisHelper.contains("NativeStructureLocateResults.selectAndReference("));
|
||||
assertTrue(irisHelper.contains("selectedSearch.search().reference(selectedStart)"));
|
||||
assertTrue(irisHelper.contains("findUnexplored"));
|
||||
assertTrue(irisHelper.contains("verified.ownership().locatorY()"));
|
||||
assertFalse(outerMethod.contains("delegate.findNearestMapStructure("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||
|
||||
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
AtomicInteger irisReferences = new AtomicInteger();
|
||||
AtomicInteger nativeReferences = new AtomicInteger();
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> irisSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeFar, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(irisNear, irisSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(0, nativeReferences.get());
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeNear, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(nativeNear, nativeSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(1, nativeReferences.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePredictionIsReadOnlyUntilTheWinnerIsCommitted() throws IOException {
|
||||
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
|
||||
String source = Files.readString(nativegen.resolve("NativeStructureVanillaLocator.java")).replace("\r\n", "\n");
|
||||
int predictionStart = source.indexOf("private static Candidate predictAt(");
|
||||
int candidateStart = source.indexOf("public static final class Candidate", predictionStart);
|
||||
String prediction = source.substring(predictionStart, candidateStart);
|
||||
String candidate = source.substring(candidateStart);
|
||||
|
||||
assertFalse(prediction.contains("addReference("));
|
||||
assertTrue(candidate.contains("structureManager.addReference(referenceStart)"));
|
||||
assertTrue(candidate.contains("committed.compareAndSet(false, true)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
|
||||
Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"));
|
||||
String source = Files.readString(processor).replace("\r\n", "\n");
|
||||
String foundation = Files.readString(
|
||||
processor.resolveSibling("NativeStructureFoundationBuilder.java")).replace("\r\n", "\n");
|
||||
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
|
||||
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
|
||||
int stiltDefinition = foundation.indexOf("static void placeStilts(");
|
||||
int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition);
|
||||
int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition);
|
||||
|
||||
assertTrue(placement >= 0);
|
||||
assertTrue(stiltPlacement > placement);
|
||||
assertTrue(stiltDefinition >= 0);
|
||||
assertTrue(occupancyCheck > stiltDefinition);
|
||||
assertTrue(terrainFloor > stiltDefinition);
|
||||
assertFalse(source.contains("state.equals("));
|
||||
assertFalse(foundation.contains("state.equals("));
|
||||
assertFalse(source.contains("snapshot.states"));
|
||||
assertFalse(foundation.contains("snapshot.states"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))
|
||||
.resolveSibling("NativeStructureVerticalPlacer.java")).replace("\r\n", "\n");
|
||||
int placementStart = source.indexOf("public static int applyVerticalPlacement");
|
||||
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
|
||||
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
|
||||
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
|
||||
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
|
||||
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
|
||||
|
||||
assertTrue(placementStart >= 0);
|
||||
assertTrue(shiftStart > placementStart);
|
||||
assertTrue(alignmentStart > shiftStart);
|
||||
assertTrue(moveStart > alignmentStart);
|
||||
assertTrue(movePieceStart > moveStart);
|
||||
assertTrue(monumentChildrenStart > movePieceStart);
|
||||
String placementMethod = source.substring(placementStart, shiftStart);
|
||||
String shiftMethod = source.substring(shiftStart, alignmentStart);
|
||||
String moveMethod = source.substring(moveStart, movePieceStart);
|
||||
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
|
||||
|
||||
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
|
||||
assertTrue(placementMethod.contains("return applyVerticalShift("));
|
||||
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
|
||||
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
|
||||
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
|
||||
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
|
||||
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingPlacementFailureContractTest {
|
||||
@Test
|
||||
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("public int[] placeStructure(");
|
||||
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int catchStart = method.indexOf("catch (RuntimeException e)");
|
||||
|
||||
assertTrue(catchStart >= 0);
|
||||
String failurePath = method.substring(catchStart);
|
||||
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
|
||||
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
|
||||
assertFalse(failurePath.contains("return null;"));
|
||||
assertFalse(failurePath.contains("catch (Throwable"));
|
||||
}
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingServerLevelAdviceContractTest {
|
||||
@Test
|
||||
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String transformer = section(
|
||||
source,
|
||||
"public boolean injectBukkit()",
|
||||
"public void ensureServerLevelInjection()");
|
||||
String advice = section(
|
||||
source,
|
||||
"private static class ServerLevelAdvice",
|
||||
"\n }\n}");
|
||||
|
||||
assertTrue(transformer.contains("takesArgument(0, MinecraftServer.class)"));
|
||||
assertTrue(transformer.contains("takesArgument(5, LevelStem.class)"));
|
||||
assertFalse(transformer.contains("takesArgument(12, ChunkGenerator.class)"));
|
||||
assertFalse(transformer.contains("takesArgument(13, ChunkGenerator.class)"));
|
||||
assertTrue(advice.contains("@Advice.AllArguments Object[] constructorArguments"));
|
||||
assertTrue(advice.contains("argument instanceof ChunkGenerator candidate"));
|
||||
assertFalse(advice.contains("@Advice.Argument(12)"));
|
||||
assertFalse(advice.contains("@Advice.Argument(13)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String transformer = section(
|
||||
source,
|
||||
"public boolean injectBukkit()",
|
||||
"public void ensureServerLevelInjection()");
|
||||
String advice = section(
|
||||
source,
|
||||
"private static class LevelStorageAccessAdvice",
|
||||
"private static class ServerLevelAdvice");
|
||||
|
||||
assertTrue(transformer.contains(
|
||||
".type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))"));
|
||||
assertTrue(transformer.contains("Advice.to(LevelStorageAccessAdvice.class)"));
|
||||
assertTrue(transformer.contains("ElementMatchers.takesArguments(4)"));
|
||||
assertTrue(transformer.contains(
|
||||
"ElementMatchers.takesArgument(0, LevelStorageSource.class)"));
|
||||
assertTrue(transformer.contains("ElementMatchers.takesArgument(1, String.class)"));
|
||||
assertTrue(transformer.contains("ElementMatchers.takesArgument(2, Path.class)"));
|
||||
assertTrue(transformer.contains("ElementMatchers.takesArgument(3, ResourceKey.class)"));
|
||||
assertTrue(advice.contains("@Advice.Argument(1) String levelId"));
|
||||
assertTrue(advice.contains(
|
||||
"@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType"));
|
||||
assertTrue(advice.contains("getDeclaredMethod(\"peekStemGenerator\", String.class)"));
|
||||
assertTrue(advice.contains(".invoke(null, levelId)"));
|
||||
assertTrue(advice.contains(
|
||||
"dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);"));
|
||||
assertFalse(advice.contains("dimensionRoot("));
|
||||
assertFalse(advice.contains("Files.copy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String advice = section(
|
||||
source,
|
||||
"private static class ServerLevelAdvice",
|
||||
"\n }\n}");
|
||||
|
||||
assertTrue(advice.contains(
|
||||
"@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey"));
|
||||
assertTrue(advice.contains("getMethod(\"getTarget\")"));
|
||||
assertTrue(advice.contains("getMethod(\"getWorld\")"));
|
||||
assertTrue(advice.contains("getMethod(\"identity\")"));
|
||||
assertTrue(advice.contains("\"iris\".equals(worldIdentifier.getNamespace())"));
|
||||
assertTrue(advice.contains("\"minecraft\".equals(worldIdentifier.getNamespace())"));
|
||||
|
||||
int resolvedStem = advice.indexOf("Object resolvedStem = stemMethod.invoke(");
|
||||
int validatedStem = advice.indexOf("resolvedStem instanceof LevelStem runtimeStem");
|
||||
int identityPublication = advice.indexOf(
|
||||
"dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);");
|
||||
int stemPublication = advice.indexOf("levelStem = runtimeStem;");
|
||||
assertTrue(resolvedStem >= 0);
|
||||
assertTrue(validatedStem > resolvedStem);
|
||||
assertTrue(identityPublication > validatedStem);
|
||||
assertTrue(stemPublication > identityPublication);
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
assertTrue("Missing source section starting with " + startMarker, start >= 0);
|
||||
assertTrue("Missing source section ending with " + endMarker, end > start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingShutdownBoundaryContractTest {
|
||||
@Test
|
||||
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
|
||||
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
|
||||
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
||||
assertTrue(boundary.contains("server.getRunningThread()"));
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
int start = source.indexOf(startMarker);
|
||||
int end = source.indexOf(endMarker, start);
|
||||
assertTrue("Missing source section starting with " + startMarker, start >= 0);
|
||||
assertTrue("Missing source section ending with " + endMarker, end > start);
|
||||
return source.substring(start, end);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingStructureFailureContractTest {
|
||||
@Test
|
||||
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
List<String> methodNames = List.of(
|
||||
"getStructureKeys",
|
||||
"getStructureSetKeys",
|
||||
"getReachableStructureKeys",
|
||||
"getStructureBiomeKeys",
|
||||
"getPossibleBiomeKeys");
|
||||
|
||||
for (String methodName : methodNames) {
|
||||
String method = methodSource(source, methodName);
|
||||
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
|
||||
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
|
||||
assertFalse(methodName + " must not log and return an empty registry result",
|
||||
method.contains("IrisLogging.reportError"));
|
||||
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String methodSource(String source, String methodName) {
|
||||
int start = source.indexOf("public KList<String> " + methodName + "(");
|
||||
int end = source.indexOf("\n @Override", start + 1);
|
||||
if (start < 0 || end < 0) {
|
||||
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
|
||||
}
|
||||
return source.substring(start, end);
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingTileDeserializeContractTest {
|
||||
@Test
|
||||
public void ownedRegionTileDataMergesBeforeScheduledFallback() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("public void deserializeTile(");
|
||||
int methodEnd = source.indexOf("\n private void merge(", methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
|
||||
int ownershipCheck = method.indexOf("J.isOwnedByCurrentRegion(");
|
||||
int synchronousMerge = method.indexOf("merge(level, blockPos, tag);", ownershipCheck);
|
||||
int synchronousReturn = method.indexOf("return;", synchronousMerge);
|
||||
int scheduledFallback = method.indexOf("J.runAt(pos, () -> merge(level, blockPos, tag))");
|
||||
|
||||
assertTrue(ownershipCheck >= 0);
|
||||
assertTrue(synchronousMerge > ownershipCheck);
|
||||
assertTrue(synchronousReturn > synchronousMerge);
|
||||
assertTrue(scheduledFallback > synchronousReturn);
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* A registered structure with an EMPTY biome filter is legal datapack content: opt-in structures
|
||||
* (e.g. Towns and Towers' towns_and_towers:exclusives/*) ship biome tags that only reference
|
||||
* optional modded biomes, so the filter resolves to zero entries on a vanilla server. Resolving
|
||||
* biome keys for such a structure must return an empty set (the structure is simply unreachable),
|
||||
* never throw — a throw here aborts the whole /iris structure verify run and degrades /iris find.
|
||||
* The defensive throw is only kept for a NON-empty filter whose holders all fail to resolve keys.
|
||||
*/
|
||||
public class VanillaStructureBiomesEmptyFilterContractTest {
|
||||
@Test
|
||||
public void emptyBiomeFilterReturnsEmptyKeysInsteadOfThrowing() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.vanillaStructureBiomesSource")));
|
||||
|
||||
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
|
||||
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
|
||||
assertFalse(source.contains("exposes no registered biome keys"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moddedHookMatchesEmptyFilterContract() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.moddedStructureHooksSource")));
|
||||
|
||||
assertTrue(source.contains("if (keys.isEmpty() && hasFilterEntries)"));
|
||||
assertTrue(source.contains("has biome filter entries but none resolve to registered biome keys"));
|
||||
assertFalse(source.contains("exposes no registered biome keys"));
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
}
|
||||
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
|
||||
|
||||
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
when(fixture.complex().resolvedTerrainColumn(0, 0))
|
||||
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
|
||||
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
|
||||
|
||||
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
||||
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
LevelHeightAccessor.create(-16, 2), null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
LevelHeightAccessor.create(-14, 3), null));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
|
||||
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
set(generator, "engine", engine);
|
||||
set(generator, "terrainHeights", new NativeTerrainHeightCache());
|
||||
return new Fixture(generator, engine, complex);
|
||||
}
|
||||
|
||||
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
|
||||
private static TerrainBoundarySignature signature() {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
|
||||
return new TerrainBoundarySignature(
|
||||
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
|
||||
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
|
||||
}
|
||||
|
||||
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.world.history.GenerationActivation;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeGenerationWriteGuardTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
|
||||
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
when(objects.contains(-3, 74, -7)).thenReturn(true);
|
||||
|
||||
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||
|
||||
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
|
||||
verifyNoInteractions(stack);
|
||||
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
|
||||
.test(new BlockPos(-3, 11, -7)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
|
||||
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
DimensionStackLayout negative = mock(DimensionStackLayout.class);
|
||||
DimensionStackLayout positive = mock(DimensionStackLayout.class);
|
||||
when(objects.isEmpty()).thenReturn(true);
|
||||
when(stack.sample(-1, -1)).thenReturn(negative);
|
||||
when(stack.sample(-1, 1)).thenReturn(positive);
|
||||
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
|
||||
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
|
||||
|
||||
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||
|
||||
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
|
||||
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
|
||||
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
|
||||
verify(stack, times(1)).sample(-1, -1);
|
||||
verify(stack, times(1)).sample(-1, 1);
|
||||
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
GenerationHistory history = mock(GenerationHistory.class);
|
||||
GenerationActivation active = mock(GenerationActivation.class);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.history()).thenReturn(history);
|
||||
when(history.activeActivation()).thenReturn(active);
|
||||
when(active.activationId()).thenReturn(4L);
|
||||
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
WorldGenLevel region = mock(WorldGenLevel.class);
|
||||
ChunkAccess partial = mock(ChunkAccess.class);
|
||||
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
ChunkAccess complete = mock(ChunkAccess.class);
|
||||
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
when(region.getChunk(1, -1)).thenReturn(complete);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
|
||||
}
|
||||
}
|
||||
+2
-39
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisJigsawConfiguration;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
@@ -59,9 +59,6 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -316,40 +313,6 @@ public class NativeStructureFactoryTest {
|
||||
assertEquals(1, oversized.getPieces().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedManualGenerationClearsStaleOwnershipAfterReplacementHandling() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
|
||||
StandardCharsets.UTF_8);
|
||||
int failureStart = source.indexOf("if (!isUsableGeneratedStart(generated))");
|
||||
int failureEnd = source.indexOf("continue;", failureStart);
|
||||
String failureBranch = source.substring(failureStart, failureEnd);
|
||||
|
||||
assertTrue(failureBranch.contains("if (replacement)"));
|
||||
assertTrue(failureBranch.indexOf("NativeStructureOwnershipStore.discard(")
|
||||
> failureBranch.lastIndexOf("}") );
|
||||
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipFingerprint.capture("));
|
||||
assertTrue(failureStart < source.indexOf("NativeStructureOwnershipStore.record("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedManualStartPublicationClearsRecordedOwnershipAndPreservesTheFailure() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nativeStructureStartInjectorSource")),
|
||||
StandardCharsets.UTF_8);
|
||||
int ownershipRecord = source.indexOf("NativeStructureOwnershipStore.record(");
|
||||
int publication = source.indexOf("context.structureManager().setStartForStructure(", ownershipRecord);
|
||||
int cleanup = source.indexOf("NativeStructureOwnershipStore.discard(", publication);
|
||||
int suppressed = source.indexOf("publicationError.addSuppressed(cleanupError)", cleanup);
|
||||
int rethrow = source.indexOf("throw publicationError;", suppressed);
|
||||
|
||||
assertTrue(ownershipRecord >= 0);
|
||||
assertTrue(publication > ownershipRecord);
|
||||
assertTrue(cleanup > publication);
|
||||
assertTrue(suppressed > cleanup);
|
||||
assertTrue(rethrow > suppressed);
|
||||
}
|
||||
|
||||
private static final class TestChunkGenerator extends ChunkGenerator {
|
||||
private TestChunkGenerator() {
|
||||
super(new FixedBiomeSource(Holder.direct((Biome) null)));
|
||||
|
||||
-17
@@ -9,9 +9,6 @@ import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -24,20 +21,6 @@ public class NativeStructureGenerationKeysContractTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachabilityUsesEnabledActiveStructureSetsWithoutLazyPlacementGeneration() throws IOException {
|
||||
String source = Files.readString(Path.of(
|
||||
System.getProperty("iris.nativeStructureGenerationKeysSource")));
|
||||
|
||||
assertTrue(source.contains("getWorldGenSettings().options().generateStructures()"));
|
||||
assertTrue(source.contains("getGeneratorState().possibleStructureSets()"));
|
||||
assertTrue(source.contains("isEnabledPlacement(structureSet.placement())"));
|
||||
assertTrue(source.contains("StructurePlacement.class.getDeclaredMethods()"));
|
||||
assertTrue(source.contains("entry.weight() <= 0"));
|
||||
assertTrue(source.contains("hasPossibleBiome(entry.structure().value(), possibleBiomes)"));
|
||||
assertFalse(source.contains("getPlacementsForStructure("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
|
||||
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.placement.StructurePlacementGrid;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
+4
-19
@@ -1,9 +1,9 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.engine.object.IrisStructureYBand;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.structure.placement.IrisStructureYBand;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -39,11 +39,8 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProc
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -221,18 +218,6 @@ public class NativeStructurePostProcessorEncaseTest {
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseFillRunsBeforeNativePlacement() throws IOException {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))).replace("\r\n", "\n");
|
||||
int prepareTerrain = source.indexOf("NativeStructurePostProcessor.prepareTerrain(");
|
||||
int placementLoop = source.indexOf("for (NativePlacementGroup group : placementGroups)", prepareTerrain);
|
||||
int placement = source.indexOf("placeVanillaStructure(world, structureManager, random", placementLoop);
|
||||
|
||||
assertTrue(prepareTerrain >= 0);
|
||||
assertTrue(placementLoop > prepareTerrain);
|
||||
assertTrue(placement > placementLoop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void omittedTerrainConfigurationRetainsSourceSemantics() {
|
||||
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
|
||||
+535
-10
@@ -1,9 +1,9 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint;
|
||||
import art.arcane.iris.engine.object.IrisStructureCarveShape;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrain;
|
||||
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.generation.mantle.StructureCarvingFootprint;
|
||||
import art.arcane.iris.structure.placement.IrisStructureCarveShape;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
@@ -30,7 +31,9 @@ import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
@@ -51,6 +54,9 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.ProcessorRule
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.ShipwreckStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||
@@ -87,7 +93,7 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
private static final int SLAB_DEPTH = 128;
|
||||
private static final int SLAB_DEPTH = 512;
|
||||
private static final int SLAB_MIN_Y = 64;
|
||||
private static final int SLAB_PADDING = 14;
|
||||
private static final int SLAB_WIDTH = 16;
|
||||
@@ -99,6 +105,512 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenCutsAndFillsSymmetricallyAcrossItsConfiguredBlend() {
|
||||
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 48);
|
||||
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = List.of(anchor);
|
||||
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 128).targetY());
|
||||
assertEquals(80, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 32).targetY());
|
||||
assertEquals(152, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 200).targetY());
|
||||
assertEquals(48, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 0).targetY());
|
||||
int previousCut = 80;
|
||||
for (int x = 1; x <= 24; x++) {
|
||||
int cut = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 128).targetY();
|
||||
int fill = NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, 0, 32).targetY();
|
||||
assertEquals(160, cut + fill);
|
||||
assertTrue(cut >= previousCut);
|
||||
assertTrue(cut - previousCut <= 3);
|
||||
previousCut = cut;
|
||||
}
|
||||
assertEquals(128, previousCut);
|
||||
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 24, 0, 128).range());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenBlendsNeighboringFoundationsWithoutOrderDependentRidges() {
|
||||
NativeStructureSurfaceFitter.FlattenAnchor left = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2), 24, 64);
|
||||
NativeStructureSurfaceFitter.FlattenAnchor right = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(8, 8, 0, 0, 80, 2), 24, 64);
|
||||
int previous = 80;
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
NativeStructureSurfaceFitter.FlattenResolution forward = NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
List.of(left, right), x, 0, 104);
|
||||
assertEquals(forward, NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
List.of(right, left), x, 0, 104));
|
||||
assertTrue(forward.targetY() <= 82);
|
||||
assertTrue(Math.abs(forward.targetY() - previous) <= 1);
|
||||
previous = forward.targetY();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenBlendsAnElevatedRoadJunctionWithNearbyFoundations() {
|
||||
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = new ArrayList<>();
|
||||
for (int x : List.of(-3, 3)) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(x, x, z, z, 64, 2), 24, 64));
|
||||
}
|
||||
}
|
||||
anchors.add(new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64));
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
NativeStructureSurfaceFitter.FlattenResolution grade =
|
||||
NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, x, z, 96);
|
||||
assertTrue(grade.targetY() >= 64 && grade.targetY() <= 72);
|
||||
assertEquals(grade, NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
anchors.reversed(), x, z, 96));
|
||||
if (x < 1) {
|
||||
assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
anchors, x + 1, z, 96).targetY()) <= 1);
|
||||
}
|
||||
if (z < 1) {
|
||||
assertTrue(Math.abs(grade.targetY() - NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
anchors, x, z + 1, 96).targetY()) <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenPreservesAnExactRigidFloorBesideAnElevatedJunction() {
|
||||
NativeStructureSurfaceFitter.FlattenAnchor rigid = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 24, 64);
|
||||
NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), 24, 64);
|
||||
assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
List.of(rigid, junction), 0, 0, 96).targetY());
|
||||
assertEquals(64, NativeStructureSurfaceFitter.resolveFlattenSurface(
|
||||
List.of(junction, rigid), 0, 0, 96).targetY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenRetainsIsolatedJunctionSupportWithinItsConfiguredBounds() {
|
||||
for (int radius : List.of(0, 24)) {
|
||||
NativeStructureSurfaceFitter.FlattenAnchor junction = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 105, 1), radius, 8);
|
||||
List<NativeStructureSurfaceFitter.FlattenAnchor> anchors = List.of(junction);
|
||||
assertEquals(104, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).targetY());
|
||||
assertEquals(8, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, 0, 0, 96).range());
|
||||
int outside = Math.max(1, radius);
|
||||
assertEquals(96, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).targetY());
|
||||
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(anchors, outside, 0, 96).range());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenLevelsNonJigsawFoundationsAndRemovesDetachedRoofs() {
|
||||
StructureStart start = desertStart();
|
||||
int groundY = start.getBoundingBox().minY() - 1;
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 3, 127, 0);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = 0; x <= 3; x++) {
|
||||
int sourceY = x < 2 ? groundY - 32 : groundY + 32;
|
||||
put(blocks, x, 12, 0, Blocks.STONE.defaultBlockState());
|
||||
put(blocks, x, sourceY - 1, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, sourceY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
assertTrue(NativeStructureSurfaceFitter.requiresFlattenTerrain(
|
||||
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
|
||||
(x, z) -> x < 2 ? groundY - 32 : groundY + 32);
|
||||
|
||||
for (int x = 0; x <= 3; x++) {
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, x, groundY, 0));
|
||||
for (int y = groundY + 1; y <= groundY + 32; y++) {
|
||||
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
|
||||
}
|
||||
for (int y = 12; y < groundY; y++) {
|
||||
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenRestoresAnUnchangedFoundationSurfaceOverADeepAirGap() {
|
||||
StructureStart start = desertStart();
|
||||
int groundY = start.getBoundingBox().minY() - 1;
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, 0, groundY - 19, 0, Blocks.DIRT.defaultBlockState());
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY);
|
||||
|
||||
for (int y = groundY - 19; y <= groundY; y++) {
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, y, 0));
|
||||
}
|
||||
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
|
||||
StructureStart start = desertStart();
|
||||
int groundY = start.getBoundingBox().minY() - 1;
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
|
||||
Map<BlockPos, BlockState> buried = flatTerrain(area, 100);
|
||||
Map<BlockPos, BlockState> originalBuried = new HashMap<>(buried);
|
||||
Map<BlockPos, BlockState> submerged = flatTerrain(area, groundY - 8);
|
||||
put(submerged, 0, groundY - 7, 0, Blocks.WATER.defaultBlockState());
|
||||
Map<BlockPos, BlockState> originalSubmerged = new HashMap<>(submerged);
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(buried), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> 100);
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(submerged), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)), (x, z) -> groundY - 8);
|
||||
|
||||
assertEquals(originalBuried, buried);
|
||||
assertEquals(originalSubmerged, submerged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenExemptsUndergroundAndSubmergedStructureTypes() {
|
||||
Structure.StructureSettings surface = new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE);
|
||||
Structure.StructureSettings underground = new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.UNDERGROUND_STRUCTURES, TerrainAdjustment.NONE);
|
||||
List<Structure> exempt = List.of(new StrongholdStructure(surface), new OceanMonumentStructure(surface),
|
||||
new ShipwreckStructure(surface, false), new DesertPyramidStructure(underground));
|
||||
for (Structure structure : exempt) {
|
||||
StructureStart start = new StructureStart(structure, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(desertStart().getPieces()));
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
|
||||
surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)));
|
||||
}
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
|
||||
surfaceTarget(desertStart(TerrainAdjustment.BURY), IrisStructureTerrainMode.FLATTEN)));
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresFlattenTerrain(
|
||||
surfaceTarget(desertStart(TerrainAdjustment.ENCAPSULATE), IrisStructureTerrainMode.FLATTEN)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenFoundationReadsAndWritesStayInsideConfiguredVerticalRange() throws Exception {
|
||||
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 128, 0, 0, 136, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(piece));
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setFlattenRange(32);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 0, 255, 0);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, 0, 94, 0, Blocks.GOLD_BLOCK.defaultBlockState());
|
||||
WorldGenLevel delegate = world(blocks);
|
||||
WorldGenLevel bounded = (WorldGenLevel) Proxy.newProxyInstance(WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class}, (proxy, method, arguments) -> {
|
||||
if (method.getName().equals("getBlockState") || method.getName().equals("setBlock")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
assertTrue(position.toString(), position.getY() >= 95 && position.getY() <= 159);
|
||||
}
|
||||
return method.invoke(delegate, arguments);
|
||||
});
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(bounded, area, List.of(target), (x, z) -> 127);
|
||||
|
||||
assertEquals(Blocks.GOLD_BLOCK.defaultBlockState(), state(blocks, 0, 94, 0));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 95, 0));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(), state(blocks, 0, 127, 0));
|
||||
assertTrue(state(blocks, 0, 128, 0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenReferencesIncludeTheMaximumConfiguredFeather() throws Exception {
|
||||
PoolElementStructurePiece piece = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(piece));
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)
|
||||
.setHorizontalPadding(128).setFlattenRange(128);
|
||||
|
||||
BoundingBox references = NativeStructureReferenceEnvelope.referenceBounds(start, start.getStructure(), terrain);
|
||||
|
||||
assertEquals(-128, references.minX());
|
||||
assertEquals(128, references.maxX());
|
||||
assertEquals(-128, references.minZ());
|
||||
assertEquals(128, references.maxZ());
|
||||
NativeStructureSurfaceFitter.FlattenAnchor anchor = new NativeStructureSurfaceFitter.FlattenAnchor(
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 64, 2), 128, 128);
|
||||
assertTrue(NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 127, 0, 100).range() > 0);
|
||||
assertEquals(0, NativeStructureSurfaceFitter.resolveFlattenSurface(List.of(anchor), 128, 0, 100).range());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenPreservesNativeVillageSupportBridges() throws Exception {
|
||||
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
|
||||
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
|
||||
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
|
||||
BoundingBox area = new BoundingBox(0, 58, 0, 0, 72, 0);
|
||||
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
|
||||
|
||||
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
|
||||
assertEquals(Set.of(BlockPos.asLong(0, 63, 0)), written);
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 0, 63, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenDoesNotBridgeACompletelyBuriedVillage() throws Exception {
|
||||
PoolElementStructurePiece lower = rigidTemplatePiece(new InlineLegacyPoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.AIR.defaultBlockState())))),
|
||||
new BoundingBox(0, 62, 0, 0, 65, 0), 1, Rotation.NONE);
|
||||
PoolElementStructurePiece upper = rigidTemplatePiece(new InlineSinglePoolElement(template(List.of(
|
||||
block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 64, 0, 0, 70, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(lower, upper));
|
||||
BoundingBox area = new BoundingBox(0, 58, 0, 0, 90, 0);
|
||||
Map<BlockPos, BlockState> blocks = supportTerrain(0, 0);
|
||||
put(blocks, 0, 80, 0, Blocks.STONE.defaultBlockState());
|
||||
|
||||
Set<Long> written = NativeStructureSurfaceSupportBuilder.bridgeRigidPieceSupport(world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
|
||||
|
||||
assertTrue(written.isEmpty());
|
||||
assertTrue(state(blocks, 0, 63, 0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenKeepsPyramidPlacementAboveItsPreparedGround() {
|
||||
FlattenDesertPiece piece = new FlattenDesertPiece();
|
||||
StructureStart start = new StructureStart(desertStart().getStructure(), new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of(piece)));
|
||||
BoundingBox footprint = piece.getBoundingBox();
|
||||
NativeStructureVerticalPlacer.applyVerticalPlacement(start, "minecraft:desert_pyramid", 0,
|
||||
63, 0, 256, false, false, null, (x, z) -> x == footprint.minX() ? 80 : 112);
|
||||
BoundingBox area = new BoundingBox(footprint.minX(), 0, footprint.minZ(),
|
||||
footprint.maxX(), 160, footprint.maxZ());
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = footprint.minX(); x <= footprint.maxX(); x++) {
|
||||
for (int z = footprint.minZ(); z <= footprint.maxZ(); z++) {
|
||||
int surfaceY = x == footprint.minX() ? 80 : 112;
|
||||
put(blocks, x, surfaceY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, surfaceY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.FLATTEN)),
|
||||
(x, z) -> x == footprint.minX() ? 80 : 112);
|
||||
|
||||
assertEquals(81, piece.getBoundingBox().minY());
|
||||
assertTrue(piece.attemptVanillaAlignment());
|
||||
assertEquals(81, piece.getBoundingBox().minY());
|
||||
assertTrue(state(blocks, footprint.maxX(), 80, footprint.maxZ()).isSolid());
|
||||
assertTrue(state(blocks, footprint.maxX(), 81, footprint.maxZ()).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenLetsTerrainMatchingPathsFollowThePreparedVillageGrade() throws Exception {
|
||||
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
|
||||
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
|
||||
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
|
||||
new BoundingBox(4, 65, 0, 4, 75, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setHorizontalPadding(24);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 8, 128, 0);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 96);
|
||||
for (int x : List.of(0, 8)) {
|
||||
blocks.remove(new BlockPos(x, 95, 0));
|
||||
blocks.remove(new BlockPos(x, 96, 0));
|
||||
put(blocks, x, 63, 0, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
WorldGenLevel world = world(blocks);
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(world, area, List.of(target),
|
||||
(x, z) -> x == 0 || x == 8 ? 64 : 96);
|
||||
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
|
||||
world, path, BlockPos.ZERO, area,
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
|
||||
area::isInside, cells -> {});
|
||||
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
|
||||
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
|
||||
|
||||
assertTrue(state(blocks, 4, 96, 0).isAir());
|
||||
assertEquals(1, pathGround.size());
|
||||
int pathY = pathGround.values().iterator().next().y();
|
||||
assertTrue("Path Y " + pathY, pathY >= 64 && pathY <= 67);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenPublishesCutAndFillHeightsBeforeNativePathProjectionAtFeatures() throws Exception {
|
||||
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(0, 256), containerFactory(), null);
|
||||
chunk.setPersistedStatus(ChunkStatus.CARVERS);
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
int originalY = x < 4 ? 96 : 32;
|
||||
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
|
||||
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk,
|
||||
(x, z) -> x == 15 ? 81 : x < 4 ? 97 : 33,
|
||||
(x, z) -> x == 15 ? 61 : x < 4 ? 97 : 33);
|
||||
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
|
||||
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
|
||||
block(8, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
|
||||
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
|
||||
new BoundingBox(0, 65, 0, 8, 75, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setHorizontalPadding(24);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
|
||||
WorldGenLevel world = world(chunk);
|
||||
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world, area, List.of(target), (x, z) -> x < 4 ? 96 : 32);
|
||||
|
||||
assertEquals(96, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 0, 0));
|
||||
assertEquals(32, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 0));
|
||||
plan.primeHeightmaps(chunk);
|
||||
chunk.setBlockState(new BlockPos(0, 85, 0), Blocks.OAK_PLANKS.defaultBlockState(), 2);
|
||||
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
|
||||
world, path, BlockPos.ZERO, area,
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
|
||||
area::isInside, cells -> {});
|
||||
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
|
||||
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
|
||||
|
||||
assertEquals(2, pathGround.size());
|
||||
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
|
||||
assertEquals(64, cell.y());
|
||||
chunk.setBlockState(new BlockPos(cell.x(), cell.y(), cell.z()), cell.occupancy().state(), 2);
|
||||
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 63, cell.z())).isSolid());
|
||||
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 65, cell.z())).isAir());
|
||||
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), 66, cell.z())).isAir());
|
||||
assertEquals(64, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, cell.x(), cell.z()));
|
||||
assertEquals(64, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, cell.x(), cell.z()));
|
||||
}
|
||||
assertEquals(80, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 15, 0));
|
||||
assertEquals(60, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, 15, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenSupportsProjectedRoadsAndTheirSurroundingGradeAcrossRoofCavities() throws Exception {
|
||||
ProtoChunk chunk = new ProtoChunk(new ChunkPos(0, 0), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(0, 256), containerFactory(), null);
|
||||
chunk.setPersistedStatus(ChunkStatus.CARVERS);
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
int originalY = x == 0 || x == 8 ? 64 : 96;
|
||||
write(chunk, x, 20, 0, Blocks.STONE.defaultBlockState());
|
||||
write(chunk, x, originalY - 1, 0, Blocks.DIRT.defaultBlockState());
|
||||
write(chunk, x, originalY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
write(chunk, 6, 50, 0, Blocks.WATER.defaultBlockState());
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk,
|
||||
(x, z) -> x == 0 || x == 8 ? 65 : 97,
|
||||
(x, z) -> x == 0 || x == 8 ? 65 : 97);
|
||||
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
PoolElementStructurePiece left = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(0, 65, 0, 0, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece right = rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(8, 65, 0, 8, 75, 0), 0, Rotation.NONE);
|
||||
PoolElementStructurePiece path = rigidTemplatePiece(new InlineSinglePoolElement(
|
||||
template(List.of(block(0, 0, 0, Blocks.DIRT_PATH.defaultBlockState()),
|
||||
block(1, 0, 0, Blocks.AIR.defaultBlockState()),
|
||||
block(2, 0, 0, Blocks.DIRT_PATH.defaultBlockState()))),
|
||||
List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING),
|
||||
new BoundingBox(4, 65, 0, 6, 75, 0), 0, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(List.of(left, path, right));
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setHorizontalPadding(24).setFlattenRange(32);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 0);
|
||||
WorldGenLevel world = world(chunk);
|
||||
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world, area, List.of(target), (x, z) -> x == 0 || x == 8 ? 64 : 96);
|
||||
plan.primeHeightmaps(chunk);
|
||||
NativeStructureTemplateOccupancy.OccupancyResult occupancy = NativeStructureTemplateOccupancy.resolve(
|
||||
world, path, BlockPos.ZERO, area,
|
||||
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager,
|
||||
area::isInside, cells -> {});
|
||||
Map<Long, NativeStructureTemplateOccupancy.LowestCell> pathGround =
|
||||
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
|
||||
|
||||
assertEquals(2, pathGround.size());
|
||||
for (NativeStructureTemplateOccupancy.LowestCell cell : pathGround.values()) {
|
||||
write(chunk, cell.x(), cell.y(), cell.z(), cell.occupancy().state());
|
||||
assertTrue(cell.y() > 64 && cell.y() < 72);
|
||||
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() - 1, cell.z())).isSolid());
|
||||
assertTrue(chunk.getBlockState(new BlockPos(cell.x(), cell.y() + 1, cell.z())).isAir());
|
||||
}
|
||||
int roadY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 4, 0);
|
||||
for (int y = roadY - 32; y < roadY; y++) {
|
||||
assertTrue("Missing road support at " + y, chunk.getBlockState(new BlockPos(4, y, 0)).isSolid());
|
||||
}
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, roadY - 33, 0)).isAir());
|
||||
int adjacentY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 5, 0);
|
||||
for (int y = adjacentY - 32; y < adjacentY; y++) {
|
||||
assertTrue("Missing ground between road cells at " + y,
|
||||
chunk.getBlockState(new BlockPos(5, y, 0)).isSolid());
|
||||
}
|
||||
assertEquals(Blocks.WATER.defaultBlockState(), chunk.getBlockState(new BlockPos(6, 50, 0)));
|
||||
assertTrue(chunk.getBlockState(new BlockPos(6, 49, 0)).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flattenBuildsAContinuousVillageGradeBetweenSparseFoundations() throws Exception {
|
||||
StructureTemplate foundation = template(List.of(block(0, 0, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
List<PoolElementStructurePiece> pieces = new ArrayList<>();
|
||||
for (int x : List.of(0, 8)) {
|
||||
for (int z : List.of(0, 8)) {
|
||||
pieces.add(rigidTemplatePiece(new InlineSinglePoolElement(foundation),
|
||||
new BoundingBox(x, 65, z, x, 75, z), 0, Rotation.NONE));
|
||||
}
|
||||
}
|
||||
StructureStart start = rigidSurfaceStart(pieces);
|
||||
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, IrisStructureTerrainMode.FLATTEN);
|
||||
target.terrain().setHorizontalPadding(24).setFlattenRange(64);
|
||||
BoundingBox area = new BoundingBox(0, 0, 0, 8, 127, 8);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
int originalY = (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96;
|
||||
put(blocks, x, 20, z, Blocks.STONE.defaultBlockState());
|
||||
put(blocks, x, originalY - 1, z, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, originalY, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan = NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, List.of(target),
|
||||
(x, z) -> (x == 0 || x == 8) && (z == 0 || z == 8) ? 64 : 96);
|
||||
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
|
||||
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
|
||||
for (int y = 20; y <= groundY; y++) {
|
||||
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
|
||||
state(blocks, x, y, z).isSolid());
|
||||
}
|
||||
for (int y = groundY + 1; y <= 96; y++) {
|
||||
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
|
||||
state(blocks, x, y, z).isAir());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() {
|
||||
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
@@ -474,7 +986,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
BoundingBox area = new BoundingBox(0, 54, 0, 3, 74, 3);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
|
||||
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
|
||||
@@ -514,7 +1026,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
BoundingBox area = new BoundingBox(0, 54, 0, 3, 72, 3);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 60);
|
||||
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan plan =
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan plan =
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area,
|
||||
List.of(surfaceTarget(start, IrisStructureTerrainMode.VACUUM)),
|
||||
@@ -546,13 +1058,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
Map<BlockPos, BlockState> wideBlocks = flatTerrain(wideArea, 60);
|
||||
Map<BlockPos, BlockState> splitBlocks = flatTerrain(wideArea, 60);
|
||||
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan widePlan =
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan widePlan =
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(wideBlocks), wideArea, List.of(target), (x, z) -> 60);
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan westPlan =
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan westPlan =
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(splitBlocks), westArea, List.of(target), (x, z) -> 60);
|
||||
NativeStructureSurfaceFitter.VacuumFoundationPlan eastPlan =
|
||||
NativeStructureSurfaceFitter.SurfaceTerrainPlan eastPlan =
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(splitBlocks), eastArea, List.of(target), (x, z) -> 60);
|
||||
for (int x = 15; x <= 16; x++) {
|
||||
@@ -2203,6 +2715,16 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
return template;
|
||||
}
|
||||
|
||||
private static final class FlattenDesertPiece extends DesertPyramidPiece {
|
||||
private FlattenDesertPiece() {
|
||||
super(RandomSource.create(29L), 0, 0);
|
||||
}
|
||||
|
||||
private boolean attemptVanillaAlignment() {
|
||||
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, -2);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InlineSinglePoolElement extends SinglePoolElement {
|
||||
private InlineSinglePoolElement(StructureTemplate template) {
|
||||
this(template, List.of());
|
||||
@@ -2319,6 +2841,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
if (methodName.equals("getSeed")) {
|
||||
return TEST_SEED;
|
||||
}
|
||||
if (methodName.equals("getHeight") && arguments != null && arguments.length == 3) {
|
||||
return chunk.getHeight((Heightmap.Types) arguments[0], (int) arguments[1], (int) arguments[2]) + 1;
|
||||
}
|
||||
if (methodName.equals("getLevel")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+63
@@ -188,6 +188,69 @@ public class NativeStructurePostProcessorVegetationTest {
|
||||
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() {
|
||||
ProtoChunk chunk = chunk();
|
||||
for (int y = 80; y <= 102; y++) {
|
||||
write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState());
|
||||
}
|
||||
for (int y = 103; y <= 105; y++) {
|
||||
write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState());
|
||||
}
|
||||
write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y);
|
||||
|
||||
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4)));
|
||||
for (int y = 94; y <= 106; y++) {
|
||||
assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfacePreservesCactusOnUnchangedColumns() {
|
||||
ProtoChunk chunk = chunk();
|
||||
for (int x : List.of(4, 5)) {
|
||||
for (int y = 60; y <= 64; y++) {
|
||||
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
|
||||
}
|
||||
write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState());
|
||||
write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
}
|
||||
WorldGenLevel world = world(chunk);
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
|
||||
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir());
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir());
|
||||
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
|
||||
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
|
||||
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
|
||||
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) {
|
||||
ProtoChunk chunk = chunk();
|
||||
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
|
||||
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
|
||||
write(chunk, 4, 65, 4, boundary);
|
||||
write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState());
|
||||
write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||
|
||||
assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4)));
|
||||
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4)));
|
||||
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
world(chunk), chunk, AREA, List.of(start));
|
||||
|
||||
+418
-17
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.HolderSet;
|
||||
@@ -17,15 +19,35 @@ import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructureVolumeIndexTest {
|
||||
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
|
||||
@@ -75,6 +97,151 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertEquals(coldResolutions, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||
Engine engine = runtimeEngine(runtimeId);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
|
||||
runtimeId.set(2);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
|
||||
index.evictRuntime(1);
|
||||
runtimeId.set(1);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(867, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
|
||||
ScopedEngine scoped = scopedEngine(true, 1);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
assertEquals(289, scoped.scopes().get());
|
||||
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||
assertEquals(306, resolver.resolutions());
|
||||
assertEquals(306, scoped.scopes().get());
|
||||
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||
assertEquals(306, scoped.scopes().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
|
||||
ScopedEngine detached = scopedEngine(true, 1);
|
||||
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
|
||||
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
|
||||
scopedEngine(true, 2), detached)) {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||
assertEquals(578, scoped.scopes().get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
|
||||
ScopedEngine scoped = scopedEngine(true, 1);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
index.evictRuntime(1);
|
||||
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||
assertEquals(578, resolver.resolutions());
|
||||
assertEquals(578, scoped.scopes().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
Set<IntConsumer> listeners = new CopyOnWriteArraySet<>();
|
||||
doAnswer(invocation -> {
|
||||
listeners.add(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(engine).addGenerationRuntimeRetirementListener(any());
|
||||
doAnswer(invocation -> {
|
||||
listeners.remove(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(engine).removeGenerationRuntimeRetirementListener(any());
|
||||
NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context(
|
||||
null, null, null, null, () -> null, () -> null, () -> null);
|
||||
NativeStructureVolumeIndex.install(engine, context);
|
||||
try {
|
||||
assertEquals(1, listeners.size());
|
||||
IntConsumer original = listeners.iterator().next();
|
||||
NativeStructureVolumeIndex.invalidate(engine);
|
||||
assertEquals(1, listeners.size());
|
||||
assertFalse(listeners.contains(original));
|
||||
|
||||
NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15);
|
||||
NativeStructureVolumeIndex current = installedIndexes().get(engine);
|
||||
assertEquals(289, cache(current, "originCache").size());
|
||||
assertEquals(1, cache(current, "queryCache").size());
|
||||
for (IntConsumer listener : listeners) {
|
||||
listener.accept(0);
|
||||
}
|
||||
assertTrue(cache(current, "originCache").isEmpty());
|
||||
assertTrue(cache(current, "queryCache").isEmpty());
|
||||
} finally {
|
||||
NativeStructureVolumeIndex.uninstall(engine);
|
||||
}
|
||||
assertTrue(listeners.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
Map<?, ?> origins = cache(index, "originCache");
|
||||
Map<?, ?> queries = cache(index, "queryCache");
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> builder = executor.submit(() -> index.originVolumes(null, 0, 0));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
index.originVolumes(null, 1, 0);
|
||||
assertEquals(1, origins.size());
|
||||
Future<?> retirement;
|
||||
synchronized (queries) {
|
||||
retirement = executor.submit(() -> index.evictRuntime(0));
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
boolean cleared = false;
|
||||
while (System.nanoTime() < deadline) {
|
||||
synchronized (origins) {
|
||||
cleared = origins.isEmpty();
|
||||
}
|
||||
if (cleared) {
|
||||
break;
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
|
||||
}
|
||||
assertTrue("Origin eviction did not complete before the query cache lock", cleared);
|
||||
resolver.release();
|
||||
assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
synchronized (origins) {
|
||||
assertTrue("The retired in-flight origin was cached again", origins.isEmpty());
|
||||
}
|
||||
}
|
||||
retirement.get(5, TimeUnit.SECONDS);
|
||||
index.originVolumes(null, 0, 0);
|
||||
assertEquals(3, resolver.resolutions());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
@@ -108,24 +275,172 @@ public class NativeStructureVolumeIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
|
||||
|
||||
assertFalse(source.contains("StructureManager"));
|
||||
assertFalse(source.contains("getStartForStructure"));
|
||||
assertFalse(source.contains("getAllStarts"));
|
||||
assertFalse(source.contains("ChunkAccess"));
|
||||
assertFalse(source.contains("NativeStructureOwnershipStore"));
|
||||
assertFalse(source.contains("NativeStructureOwnershipRecovery"));
|
||||
assertFalse(resolver.awaitSecondEntry(250));
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertEquals(306, resolver.resolutions());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaVolumesAreGatedOnPackPolicy() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
|
||||
assertQueuedTerrainCanComplete(false);
|
||||
}
|
||||
|
||||
assertTrue(source.contains("NativeStructureGenerationPolicy.resolve("));
|
||||
assertTrue(source.contains("if (!decision.generate())"));
|
||||
assertTrue(source.contains("isStructureChunk("));
|
||||
@Test
|
||||
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
|
||||
assertQueuedTerrainCanComplete(true);
|
||||
}
|
||||
|
||||
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
|
||||
CountDownLatch ownerEntered = new CountDownLatch(1);
|
||||
CompletableFuture<Void> terrain = new CompletableFuture<>();
|
||||
AtomicBoolean firstOrigin = new AtomicBoolean(true);
|
||||
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||
if (firstOrigin.compareAndSet(true, false)) {
|
||||
ownerEntered.countDown();
|
||||
terrain.join();
|
||||
}
|
||||
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||
-256, 0, -256, 256, 100, 256));
|
||||
});
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ForkJoinPool workers = new ForkJoinPool(2);
|
||||
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
|
||||
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> original = owner.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
|
||||
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
|
||||
firstWaiter.set(Thread.currentThread());
|
||||
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
|
||||
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
|
||||
return result;
|
||||
});
|
||||
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
|
||||
secondWaiter.set(Thread.currentThread());
|
||||
return index.resolve(null, 32, 0, 47, 15);
|
||||
});
|
||||
awaitOriginWindowWaiter(firstWaiter);
|
||||
awaitOriginWindowWaiter(secondWaiter);
|
||||
if (interruptWaiter) {
|
||||
firstWaiter.get().interrupt();
|
||||
}
|
||||
|
||||
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
|
||||
terrainTask.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
|
||||
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, adjacentVolumes.size());
|
||||
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
|
||||
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
|
||||
assertEquals(interruptWaiter, interruptedAfterQuery.get());
|
||||
} finally {
|
||||
terrain.complete(null);
|
||||
owner.shutdownNow();
|
||||
workers.shutdownNow();
|
||||
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
Thread thread = reference.get();
|
||||
if (thread != null && thread.getState() == Thread.State.WAITING) {
|
||||
for (StackTraceElement frame : thread.getStackTrace()) {
|
||||
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
|
||||
&& frame.getMethodName().equals("lockOriginWindow")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
throw new AssertionError("Native query did not wait for the occupied origin window");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
int distantChunkX = findDisjointWindow(0, 0);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> distant = executor.submit(
|
||||
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
|
||||
|
||||
assertTrue(resolver.awaitSecondEntry(5_000));
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
|
||||
|
||||
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
|
||||
|
||||
assertEquals(289, volumes.size());
|
||||
assertEquals("-8:-8", volumes.getFirst().structure());
|
||||
assertEquals("0:0", volumes.get(144).structure());
|
||||
assertEquals("8:8", volumes.getLast().structure());
|
||||
}
|
||||
|
||||
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
if (ownerRuntimeId != 0) {
|
||||
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
|
||||
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
|
||||
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
|
||||
}
|
||||
AtomicInteger scopes = new AtomicInteger();
|
||||
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
scopes.incrementAndGet();
|
||||
return null;
|
||||
});
|
||||
return new ScopedEngine(engine, scopes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
|
||||
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
|
||||
indexes.setAccessible(true);
|
||||
return (Map<Engine, NativeStructureVolumeIndex>) indexes.get(null);
|
||||
}
|
||||
|
||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return (Map<?, ?>) field.get(index);
|
||||
}
|
||||
|
||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||
@@ -134,6 +449,38 @@ public class NativeStructureVolumeIndexTest {
|
||||
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static Engine runtimeEngine(AtomicInteger runtimeId) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (method.getName().equals("getCacheID")) {
|
||||
return runtimeId.get();
|
||||
}
|
||||
if (method.getName().equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (method.getName().equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (method.getName().equals("toString")) {
|
||||
return "runtime-engine-" + runtimeId.get();
|
||||
}
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
|
||||
}
|
||||
|
||||
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final int originChunkX;
|
||||
@@ -157,4 +504,58 @@ public class NativeStructureVolumeIndexTest {
|
||||
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final CountDownLatch firstEntry = new CountDownLatch(1);
|
||||
private final CountDownLatch secondEntry = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
|
||||
private int resolutions() {
|
||||
return resolutions.get();
|
||||
}
|
||||
|
||||
private boolean awaitFirstEntry() throws InterruptedException {
|
||||
return firstEntry.await(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
|
||||
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void release() {
|
||||
release.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||
int resolution = resolutions.incrementAndGet();
|
||||
if (resolution == 1) {
|
||||
firstEntry.countDown();
|
||||
try {
|
||||
if (!release.await(5, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Timed out waiting to release native structure resolution");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
|
||||
}
|
||||
} else if (resolution == 2) {
|
||||
secondEntry.countDown();
|
||||
}
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
@Override
|
||||
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||
return new KList<>(new NativeStructureVolume(
|
||||
chunkX + ":" + chunkZ,
|
||||
0, 0, 0,
|
||||
15, 255, 15));
|
||||
}
|
||||
}
|
||||
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
||||
}
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NativeTerrainHeightCacheTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
NativeTerrainHeightCache.Query[] queries = {
|
||||
query(1, -1, 1),
|
||||
query(1, 1, -1),
|
||||
query(2, -1, 1),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
|
||||
};
|
||||
for (int index = 0; index < queries.length; index++) {
|
||||
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||
}
|
||||
for (int index = 0; index < queries.length; index++) {
|
||||
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||
}
|
||||
assertEquals(queries.length, calls.get());
|
||||
cache.evictRuntime(1);
|
||||
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
|
||||
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allIntegerResultsAreCacheable() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
|
||||
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedResolutionCanBeRetried() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
|
||||
throw new IllegalStateException("terrain unavailable");
|
||||
}));
|
||||
assertEquals(42, cached(cache, query, () -> 42));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
|
||||
entered.countDown();
|
||||
try {
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interruption);
|
||||
}
|
||||
return 9;
|
||||
}));
|
||||
assertTrue(entered.await(5, TimeUnit.SECONDS));
|
||||
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
|
||||
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
release.countDown();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedCacheKeepsRecentlyReadCoordinates() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
for (int x = 0; x < 65_536; x++) {
|
||||
cached(cache, query(1, x, 0), () -> 8);
|
||||
}
|
||||
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||
cached(cache, query(1, 65_536, 0), () -> 9);
|
||||
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableResolvedHeightIsNotCached() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
|
||||
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
|
||||
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
|
||||
}
|
||||
|
||||
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
|
||||
IntSupplier resolver) {
|
||||
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
|
||||
}
|
||||
|
||||
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
|
||||
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeTransitionColumnTest {
|
||||
@Rule
|
||||
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockOrNull(key)).thenReturn(custom);
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
TerrainBoundarySignature signature = signature(key);
|
||||
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature, height);
|
||||
|
||||
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesNativePropertiesCavesAndFluids() {
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
|
||||
LevelHeightAccessor.create(-16, 32));
|
||||
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
|
||||
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
|
||||
assertTrue(column.getBlock(0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
|
||||
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
|
||||
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
|
||||
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
|
||||
}
|
||||
|
||||
private static TerrainBoundarySignature signature(String solidState) {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
|
||||
"minecraft:water[level=3]", false)));
|
||||
return new TerrainBoundarySignature(
|
||||
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
|
||||
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
|
||||
geometry);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CustomBiomeSourceStructureContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
|
||||
IrisDimension host = new IrisDimension();
|
||||
host.setLoadKey("Host");
|
||||
IrisDimension upper = new IrisDimension();
|
||||
upper.setLoadKey("Layers/Upper");
|
||||
|
||||
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
|
||||
assertNotEquals(
|
||||
CustomBiomeSource.customBiomeKey(host, "Aurora"),
|
||||
CustomBiomeSource.customBiomeKey(upper, "Aurora")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
|
||||
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
|
||||
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
|
||||
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
|
||||
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
|
||||
}
|
||||
|
||||
private static Engine engineAnsweringNaturalFallback(
|
||||
boolean naturalFallback,
|
||||
int expectedX,
|
||||
int expectedZ
|
||||
) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (!method.getName().equals("answersFromNaturalTerrain")) {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
assertEquals(Integer.valueOf(expectedX), arguments[0]);
|
||||
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
|
||||
return naturalFallback;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisChunkGeneratorFailureContractTest {
|
||||
@Test
|
||||
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
|
||||
Semaphore admission = new Semaphore(1, true);
|
||||
admission.acquire();
|
||||
CountDownLatch exclusiveEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseExclusive = new CountDownLatch(1);
|
||||
AtomicBoolean dependentFinished = new AtomicBoolean(false);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
boolean stageReleased = false;
|
||||
|
||||
try {
|
||||
Future<?> exclusive = executor.submit(() -> {
|
||||
boolean acquired = false;
|
||||
try {
|
||||
admission.acquire();
|
||||
acquired = true;
|
||||
exclusiveEntered.countDown();
|
||||
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
if (acquired) {
|
||||
admission.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
awaitQueueLength(admission, 1);
|
||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||
outward.thenRun(() -> {
|
||||
try {
|
||||
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
|
||||
dependentFinished.set(true);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
});
|
||||
|
||||
admission.release();
|
||||
stageReleased = true;
|
||||
assertTrue(outward.complete(null));
|
||||
assertTrue(dependentFinished.get());
|
||||
releaseExclusive.countDown();
|
||||
exclusive.get(2, TimeUnit.SECONDS);
|
||||
assertEquals(1, admission.availablePermits());
|
||||
} finally {
|
||||
if (!stageReleased) {
|
||||
admission.release();
|
||||
}
|
||||
releaseExclusive.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() {
|
||||
CompletableFuture<Void> delegate = new CompletableFuture<>();
|
||||
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
|
||||
|
||||
assertTrue(delegate.cancel(false));
|
||||
assertFalse(transformed.isCancelled());
|
||||
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
}
|
||||
|
||||
private static void awaitQueueLength(Semaphore semaphore, int expected) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
assertTrue("Expected at least " + expected + " queued semaphore threads",
|
||||
semaphore.getQueueLength() >= expected);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||
|
||||
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
AtomicInteger irisReferences = new AtomicInteger();
|
||||
AtomicInteger nativeReferences = new AtomicInteger();
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> irisSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeFar, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(irisNear, irisSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(0, nativeReferences.get());
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeNear, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(nativeNear, nativeSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(1, nativeReferences.get());
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
|
||||
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisChunkGeneratorSpawnAdmissionTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ExecutorService reloader = Executors.newSingleThreadExecutor();
|
||||
AtomicInteger snapshots = new AtomicInteger();
|
||||
long previousSession = fixture.sessions.currentSessionId();
|
||||
try {
|
||||
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||
assertEquals(previousSession, fixture.readSession.get());
|
||||
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
|
||||
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
|
||||
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||
Future<?> query = owner.submit(() -> {
|
||||
assertTrue(fixture.query().isEmpty());
|
||||
assertNull(IrisContext.get());
|
||||
});
|
||||
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
|
||||
query.get(2L, TimeUnit.SECONDS);
|
||||
assertEquals(1, fixture.savedReads.get());
|
||||
assertEquals(1, fixture.routes.get());
|
||||
fixture.sessions.activateNextSession();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
}, reloader);
|
||||
|
||||
cutover.get(3L, TimeUnit.SECONDS);
|
||||
assertEquals(1, snapshots.get());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
|
||||
assertTrue(fixture.readSession.get() > previousSession);
|
||||
assertEquals(2, fixture.savedReads.get());
|
||||
assertEquals(2, fixture.routes.get());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
} finally {
|
||||
owner.shutdownNow();
|
||||
reloader.shutdownNow();
|
||||
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||
private final IrisEngine engine = mock(IrisEngine.class);
|
||||
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
|
||||
private final StructureManager structures = mock(StructureManager.class);
|
||||
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
|
||||
private final AtomicInteger savedReads = new AtomicInteger();
|
||||
private final AtomicInteger routes = new AtomicInteger();
|
||||
private final AtomicLong readSession = new AtomicLong();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
|
||||
ChunkGenerator delegate = mock(ChunkGenerator.class);
|
||||
CustomBiomeSource source = mock(CustomBiomeSource.class);
|
||||
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.biomes()).thenReturn(saved);
|
||||
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
|
||||
assertAdmitted();
|
||||
savedReads.incrementAndGet();
|
||||
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
|
||||
});
|
||||
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
|
||||
assertAdmitted();
|
||||
routes.incrementAndGet();
|
||||
return null;
|
||||
});
|
||||
when(delegate.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
|
||||
.thenReturn(expected);
|
||||
setField(generator, "engine", engine);
|
||||
setField(generator, "delegate", delegate);
|
||||
setField(generator, "customBiomeSource", source);
|
||||
}
|
||||
|
||||
private WeightedList<MobSpawnSettings.SpawnerData> query() {
|
||||
return generator.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
|
||||
}
|
||||
|
||||
private void assertAdmitted() {
|
||||
assertEquals(1, sessions.activeLeases());
|
||||
assertSame(engine, IrisContext.get().getEngine());
|
||||
readSession.set(IrisContext.get().getGenerationSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-105
@@ -1,9 +1,9 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.engine.object.IrisStructureSetFrequencyOverride;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -29,8 +29,6 @@ import org.junit.BeforeClass;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -42,7 +40,6 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingDatapackStructureScopeTest {
|
||||
private static final String SOURCE = "https://example.test/managed.zip";
|
||||
@@ -351,103 +348,6 @@ public class NMSBindingDatapackStructureScopeTest {
|
||||
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardPublishesOneDeferredStateWhileJigsawPublishesInitializedEmptyState() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
|
||||
int methodEnd = source.indexOf(
|
||||
"\n @Override\n public CompletableFuture<Void> completeStudioStructureBootstrap",
|
||||
methodStart);
|
||||
|
||||
assertTrue(methodStart >= 0);
|
||||
assertTrue(methodEnd > methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
|
||||
int jigsawMode = method.indexOf(
|
||||
"boolean jigsawStudio = platformGenerator != null");
|
||||
int jigsawOnly = method.indexOf("if (jigsawStudio)");
|
||||
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
|
||||
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
|
||||
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
|
||||
int emptyPublication = method.indexOf("stateField.set(chunkMap, bootstrapState);");
|
||||
int standardOnly = method.indexOf("else if (studioBootstrap)");
|
||||
int retention = method.indexOf("irisGenerator.retainStudioStructureState(");
|
||||
int standardPublication = method.indexOf("stateField.set(chunkMap, scopedState);");
|
||||
int immediateInitialization = method.indexOf("initializeAndPublishStructureState(");
|
||||
|
||||
assertTrue(filteredState >= 0);
|
||||
assertTrue(jigsawMode > filteredState);
|
||||
assertTrue(jigsawOnly > jigsawMode);
|
||||
assertTrue(emptyCreation > jigsawOnly);
|
||||
assertTrue(emptyFiltering > emptyCreation);
|
||||
assertTrue(emptyInitialization > emptyFiltering);
|
||||
assertTrue(emptyPublication > emptyInitialization);
|
||||
assertTrue(standardOnly > emptyPublication);
|
||||
assertTrue(retention > standardOnly);
|
||||
assertTrue(standardPublication > retention);
|
||||
assertTrue(immediateInitialization > standardPublication);
|
||||
assertFalse(method.contains("scopedState.ensureStructuresGenerated();"));
|
||||
assertFalse(method.contains("if (studioBootstrap && platformGenerator.isJigsawStudioActive())"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardCompletionReturnsTheExactActivationFutureWithoutReplacingState() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("public CompletableFuture<Void> completeStudioStructureBootstrap(World world)");
|
||||
int methodEnd = source.indexOf("\n @Override\n public void abandonStudioStructureBootstrap", methodStart);
|
||||
|
||||
assertTrue(methodStart >= 0);
|
||||
assertTrue(methodEnd > methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int retained = method.indexOf("generator.retainedStudioStructureState(level, chunkMap)");
|
||||
int emptyCompletion = method.indexOf("return CompletableFuture.completedFuture(null);");
|
||||
int activation = method.indexOf("return generator.activateStudioStructureState(retained);");
|
||||
|
||||
assertTrue(retained >= 0);
|
||||
assertTrue(emptyCompletion > retained);
|
||||
assertTrue(activation > retained);
|
||||
assertFalse(method.contains("stateField.set("));
|
||||
assertFalse(method.contains("retained.fullState()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void injectionVerifiesTheCanonicalPaperGeneratorBeforeStructureRetargeting() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("public void inject(long seed, Engine engine, World world)");
|
||||
int methodEnd = source.indexOf("\n @Override\n public DatapackStructureScopeResult", methodStart);
|
||||
|
||||
assertTrue(methodStart >= 0);
|
||||
assertTrue(methodEnd > methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
int publication = method.indexOf("worldGenContextField.set(chunkMap, newContext);");
|
||||
int canonicalRead = method.indexOf("level.getChunkSource().getGenerator()", publication);
|
||||
int identityGate = method.indexOf("activeGenerator != irisGenerator", canonicalRead);
|
||||
int structureRetarget = method.indexOf("retargetStructureCheck(level, irisGenerator)", identityGate);
|
||||
|
||||
assertTrue(publication >= 0);
|
||||
assertTrue(canonicalRead > publication);
|
||||
assertTrue(identityGate > canonicalRead);
|
||||
assertTrue(structureRetarget > identityGate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureStateRecreationUsesTheWorldOwnedSpigotConfiguration() throws IOException {
|
||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||
int methodStart = source.indexOf("private ChunkGeneratorStructureState createStructureState(");
|
||||
int methodEnd = source.indexOf(
|
||||
"\n private void initializeAndPublishStructureState(", methodStart);
|
||||
|
||||
assertTrue(methodStart >= 0);
|
||||
assertTrue(methodEnd > methodStart);
|
||||
String method = source.substring(methodStart, methodEnd);
|
||||
assertTrue(method.contains("level.spigotConfig"));
|
||||
assertFalse(method.contains("currentState.conf"));
|
||||
}
|
||||
|
||||
private static DatapackStructureScopeIndex index(
|
||||
List<String> structureKeys,
|
||||
List<String> structureSetKeys
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
|
||||
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructurePoiUpdatesTest {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
doAnswer(call -> {
|
||||
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
|
||||
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
|
||||
fixture.proto.setBlockState(position, call.getArgument(1), 0);
|
||||
return true;
|
||||
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
|
||||
|
||||
assertTrue(fixture.updates.setBlock(fixture.world, position,
|
||||
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
|
||||
}
|
||||
}
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
|
||||
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
|
||||
fixture.loaded.persistentDataContainer = restored;
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
|
||||
assertFalse(restored.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos retained = new BlockPos(-31, 5, 49);
|
||||
BlockPos replaced = new BlockPos(-30, 5, 49);
|
||||
BlockPos removed = new BlockPos(-29, 5, 49);
|
||||
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
|
||||
PoiSection existing = new PoiSection.Packed(true, List.of(
|
||||
new PoiRecord.Packed(retained, farmer, 0),
|
||||
new PoiRecord.Packed(replaced, farmer, 1),
|
||||
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
|
||||
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
|
||||
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
|
||||
for (BlockPos position : List.of(retained, replaced, removed)) {
|
||||
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
|
||||
}
|
||||
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
|
||||
fixture.write(removed, Blocks.AIR.defaultBlockState());
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
|
||||
assertTrue(existing.getType(removed).isEmpty());
|
||||
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedReconciliationRetainsIntentForRetry() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
|
||||
IllegalStateException failure = new IllegalStateException("POI storage failure");
|
||||
doThrow(failure).when(fixture.manager).add(any(), any());
|
||||
|
||||
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
|
||||
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
fixture.allowPoiAdds();
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
|
||||
4096, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
|
||||
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
|
||||
onOwner(() -> assertThrows(IllegalArgumentException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
|
||||
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
|
||||
}
|
||||
|
||||
private static void onOwner(Runnable action) throws Exception {
|
||||
FutureTask<Void> task = new FutureTask<>(action, null);
|
||||
new TickThread(task, "Native POI test").start();
|
||||
task.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||
biomeIds.add(biome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
|
||||
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, biome));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
|
||||
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
|
||||
private final LevelChunk loaded = mock(LevelChunk.class);
|
||||
private final WorldGenLevel world = mock(WorldGenLevel.class);
|
||||
private final PoiManager manager = mock(PoiManager.class);
|
||||
private final Map<Long, PoiSection> pois = new HashMap<>();
|
||||
|
||||
private Fixture() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getPoiManager()).thenReturn(manager);
|
||||
loaded.persistentDataContainer = proto.persistentDataContainer;
|
||||
when(loaded.getLevel()).thenReturn(level);
|
||||
when(loaded.getPos()).thenReturn(proto.getPos());
|
||||
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
|
||||
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
|
||||
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
|
||||
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
|
||||
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
|
||||
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
|
||||
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
|
||||
return true;
|
||||
});
|
||||
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
|
||||
when(manager.getType(any())).thenAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
PoiSection section = pois.get(SectionPos.of(position).asLong());
|
||||
return section == null ? Optional.empty() : section.getType(position);
|
||||
});
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
pois.get(SectionPos.of(position).asLong()).remove(position);
|
||||
return null;
|
||||
}).when(manager).remove(any());
|
||||
allowPoiAdds();
|
||||
}
|
||||
|
||||
private void allowPoiAdds() {
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
|
||||
.add(position, call.getArgument(1));
|
||||
}).when(manager).add(any(), any());
|
||||
}
|
||||
|
||||
private void write(BlockPos position, BlockState state) {
|
||||
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
|
||||
}
|
||||
|
||||
private long[] marker() {
|
||||
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
}
|
||||
|
||||
private PoiSection records() {
|
||||
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
-2
@@ -1,9 +1,14 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
@@ -12,10 +17,25 @@ import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.entity.EntityTypeTest;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.material.Fluid;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
@@ -30,11 +50,13 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureWorldgenAccessTest {
|
||||
@@ -49,6 +71,69 @@ public class NativeStructureWorldgenAccessTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
|
||||
int farX = (GENERATION_CENTER_X - 9) << 4;
|
||||
int farZ = (GENERATION_CENTER_Z + 4) << 4;
|
||||
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
|
||||
PoolElementStructurePiece piece = new PoolElementStructurePiece(
|
||||
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
|
||||
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
|
||||
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
|
||||
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
|
||||
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
|
||||
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
|
||||
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
|
||||
RecordingDelegate unbounded = new RecordingDelegate();
|
||||
unbounded.rejectDistantTerrainReads = true;
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
|
||||
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
|
||||
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
recording.rejectDistantTerrainReads = true;
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
|
||||
|
||||
assertEquals(0, recording.heightReads);
|
||||
assertEquals(0, recording.chunkReads);
|
||||
assertEquals(0, recording.terrainReads);
|
||||
assertEquals(0, recording.mutations);
|
||||
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
|
||||
assertEquals(1, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
recording.rejectDistantTerrainReads = true;
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
|
||||
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
|
||||
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
|
||||
|
||||
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
|
||||
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
|
||||
|
||||
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
|
||||
assertEquals(0, recording.heightReads);
|
||||
BlockPos local = generationCenter().getMiddleBlockPosition(64);
|
||||
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
|
||||
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
|
||||
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
|
||||
assertEquals(local.atY(90), localProjection.pos());
|
||||
assertEquals(1, recording.heightReads);
|
||||
assertEquals(0, recording.chunkReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -135,6 +220,54 @@ public class NativeStructureWorldgenAccessTest {
|
||||
assertEquals(1, recording.blockTicks.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||
assertEquals(0, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
BlockPos position = generationCenter().getMiddleBlockPosition(70);
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertFalse(access.removeBlock(position, false));
|
||||
assertFalse(access.destroyBlock(position, false));
|
||||
assertNull(access.getBlockEntity(position));
|
||||
assertTrue(access.getBlockEntity(position, null).isEmpty());
|
||||
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
|
||||
access.updateNeighborsAt(position.east(), Blocks.DIRT);
|
||||
access.neighborShapeChanged(
|
||||
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
|
||||
|
||||
assertEquals(1, recording.terrainReads);
|
||||
assertEquals(0, recording.mutations);
|
||||
assertEquals(0, recording.blockTicks.count());
|
||||
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
|
||||
assertEquals(1, recording.mutations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -183,7 +316,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
return NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y);
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
@@ -203,6 +338,7 @@ public class NativeStructureWorldgenAccessTest {
|
||||
private int statusChunkReads;
|
||||
private int mutations;
|
||||
private int events;
|
||||
private boolean rejectDistantTerrainReads;
|
||||
|
||||
private RecordingDelegate() {
|
||||
this.biome = Holder.direct((Biome) null);
|
||||
@@ -243,6 +379,11 @@ public class NativeStructureWorldgenAccessTest {
|
||||
return 384;
|
||||
}
|
||||
if (name.equals("getHeight")) {
|
||||
if (rejectDistantTerrainReads
|
||||
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|
||||
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
|
||||
throw new IllegalStateException("Requested chunk unavailable during world generation");
|
||||
}
|
||||
heightReads++;
|
||||
return 91;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkIOFlushTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
AtomicBoolean chunkPending = new AtomicBoolean(true);
|
||||
AtomicBoolean poiPending = new AtomicBoolean(true);
|
||||
AtomicBoolean entityPending = new AtomicBoolean(true);
|
||||
CountDownLatch chunkDrain = new CountDownLatch(1);
|
||||
CountDownLatch poiDrain = new CountDownLatch(1);
|
||||
CountDownLatch entityDrain = new CountDownLatch(1);
|
||||
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
|
||||
chunkDrain.countDown();
|
||||
return chunkPending.get();
|
||||
});
|
||||
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
|
||||
poiDrain.countDown();
|
||||
return poiPending.get();
|
||||
});
|
||||
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
|
||||
entityDrain.countDown();
|
||||
return entityPending.get();
|
||||
});
|
||||
ExecutorService io = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
|
||||
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
chunkPending.set(false);
|
||||
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
poiPending.set(false);
|
||||
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
entityPending.set(false);
|
||||
drain.get(5L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
chunkPending.set(false);
|
||||
poiPending.set(false);
|
||||
entityPending.set(false);
|
||||
io.shutdownNow();
|
||||
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
|
||||
Fixture fixture = new Fixture();
|
||||
IllegalStateException failure = new IllegalStateException("native IO failed");
|
||||
when(fixture.chunks.hasTasks()).thenThrow(failure);
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> fixture.binding.flushChunkIO(fixture.world)));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
private final CraftWorld world = mock(CraftWorld.class);
|
||||
private final ServerLevel level = mock(ServerLevel.class);
|
||||
private final RegionDataController chunks = mock(RegionDataController.class);
|
||||
private final RegionDataController pois = mock(RegionDataController.class);
|
||||
private final RegionDataController entities = mock(RegionDataController.class);
|
||||
|
||||
private Fixture() {
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
|
||||
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
|
||||
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkTaskPollingTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverOwnerPumpsOnlyTheTargetWorldQueue() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
CompletableFuture<Void> completed = new CompletableFuture<>();
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(server.getRunningThread()).thenReturn(Thread.currentThread());
|
||||
when(level.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false);
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||
assertTrue(binding.pollChunkTask(world));
|
||||
assertTrue(completed.isDone());
|
||||
assertFalse(binding.pollChunkTask(world));
|
||||
}
|
||||
verify(chunks, times(2)).pollTask();
|
||||
verify(server, never()).executeAllRecentInternalTasks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||
assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world));
|
||||
}
|
||||
verify(level, never()).getChunkSource();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foliaNeverUsesTheWorldWideTaskPump() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true);
|
||||
assertFalse(binding.pollChunkTask(world));
|
||||
}
|
||||
verifyNoInteractions(world);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.bukkit.Difficulty;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsEntitySpawnDifficultyTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
int rejected = 0;
|
||||
|
||||
for (EntityType type : EntityType.values()) {
|
||||
if (type.getEntityClass() == null) {
|
||||
continue;
|
||||
}
|
||||
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
|
||||
continue;
|
||||
}
|
||||
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
rejected++;
|
||||
}
|
||||
|
||||
assertTrue(rejected > 20);
|
||||
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
Entity created = mock(Entity.class);
|
||||
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
|
||||
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
Entity created = mock(Entity.class);
|
||||
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
|
||||
when(world.getDifficulty()).thenReturn(difficulty);
|
||||
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedBukkitSpawnFailuresStillPropagate() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
|
||||
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public class NmsSavedTerrainCheckpointTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
Path root = Path.of("checkpoint-world");
|
||||
byte[] receipt = new byte[]{1, 2, 3};
|
||||
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
|
||||
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
|
||||
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
|
||||
Set<ChunkPos> completed = new HashSet<>();
|
||||
AtomicBoolean flushed = new AtomicBoolean();
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
completed.add(saved.chunk());
|
||||
return Priority.COMPLETING;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
completed.add(unloaded.chunk());
|
||||
return Priority.COMPLETING;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
|
||||
flushed.set(true);
|
||||
return null;
|
||||
});
|
||||
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
|
||||
.thenAnswer(invocation -> {
|
||||
assertTrue(flushed.get());
|
||||
return null;
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
|
||||
|
||||
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
|
||||
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||
terrain.verifyNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
IOException failure = new IOException("storage flush failed");
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenThrow(failure);
|
||||
|
||||
assertSame(failure, assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
|
||||
terrain.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
Path root = Path.of("checkpoint-world");
|
||||
byte[] receipt = new byte[]{4, 5};
|
||||
IOException failure = new IOException("saved terrain receipt mismatch");
|
||||
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
|
||||
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenReturn(Priority.COMPLETING);
|
||||
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
|
||||
.thenThrow(failure);
|
||||
|
||||
assertSame(failure, assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
|
||||
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
|
||||
}
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.task.J;
|
||||
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import org.bukkit.World;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsSavedTerrainSnapshotTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
Thread owner = Thread.currentThread();
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||
PlatformHooks platform = mock(PlatformHooks.class);
|
||||
CompoundTag capturedData = new CompoundTag();
|
||||
CompoundTag autosave = new CompoundTag();
|
||||
List<String> submissions = new ArrayList<>();
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
return snapshot;
|
||||
});
|
||||
hooks.when(PlatformHooks::get).thenReturn(platform);
|
||||
doAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
return null;
|
||||
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||
when(snapshot.write()).thenAnswer(invocation -> {
|
||||
assertNotSame(owner, Thread.currentThread());
|
||||
assertEquals(List.of("checkpoint", "autosave"), submissions);
|
||||
return capturedData;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
|
||||
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
|
||||
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
|
||||
.thenAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
submissions.add("checkpoint");
|
||||
return null;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
|
||||
submissions.add("autosave");
|
||||
return null;
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||
|
||||
assertEquals(List.of("checkpoint"), submissions);
|
||||
assertFalse(captured.data().isDone());
|
||||
verify(snapshot, never()).write();
|
||||
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
|
||||
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||
|
||||
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
|
||||
verify(snapshot).write();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||
IllegalStateException failure = new IllegalStateException("serialization failed");
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
|
||||
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
|
||||
when(snapshot.write()).thenThrow(failure);
|
||||
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||
|
||||
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
|
||||
|
||||
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
|
||||
assertTrue(captured.data().isCompletedExceptionally());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
|
||||
World world = mock(World.class);
|
||||
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
|
||||
List<NewChunkHolder> holders = new ArrayList<>(129);
|
||||
for (int index = 0; index < 129; index++) {
|
||||
NewChunkHolder holder = mock(NewChunkHolder.class);
|
||||
setField(holder, "chunkX", index);
|
||||
holders.add(holder);
|
||||
}
|
||||
AtomicInteger requested = new AtomicInteger();
|
||||
AtomicBoolean writesBlocked = new AtomicBoolean(true);
|
||||
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
|
||||
Future<?> result = checkpoint.submit(() -> {
|
||||
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
requested.incrementAndGet();
|
||||
Runnable action = invocation.getArgument(3);
|
||||
return CompletableFuture.runAsync(action, owner);
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
|
||||
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
|
||||
firstBatchWaiting.countDown();
|
||||
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
|
||||
});
|
||||
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
|
||||
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
|
||||
assertEquals(64, requested.get());
|
||||
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
|
||||
assertFalse(result.isDone());
|
||||
writesBlocked.set(false);
|
||||
result.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(129, requested.get());
|
||||
} finally {
|
||||
writesBlocked.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(Object instance, String name, Object value) throws Exception {
|
||||
Field field = instance.getClass().getField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(instance, value);
|
||||
}
|
||||
|
||||
private static final class SaveFixture implements AutoCloseable {
|
||||
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||
private final ServerLevel level = mock(ServerLevel.class);
|
||||
|
||||
private SaveFixture() throws Exception {
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
setField(scheduler, "saveExecutor", group.createExecutor());
|
||||
setField(scheduler, "compressionExecutor", group.createExecutor());
|
||||
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
pool.halt(true);
|
||||
assertTrue(pool.joinInterruptable(5_000));
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsSavedTerrainWriteBarrierTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
|
||||
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
|
||||
ServerLevel level = level(saves, compression);
|
||||
ChunkPos chunk = new ChunkPos(4, -9);
|
||||
CountDownLatch generationEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseGeneration = new CountDownLatch(1);
|
||||
AtomicBoolean pendingWrite = new AtomicBoolean(true);
|
||||
AtomicInteger serialized = new AtomicInteger();
|
||||
AtomicInteger compressed = new AtomicInteger();
|
||||
AtomicReference<Thread> saveThread = new AtomicReference<>();
|
||||
AtomicReference<Thread> compressionThread = new AtomicReference<>();
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
|
||||
generation.queueTask(() -> {
|
||||
generationEntered.countDown();
|
||||
try {
|
||||
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(interruption);
|
||||
}
|
||||
});
|
||||
pool.adjustThreadCount(1);
|
||||
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
|
||||
saves.queueTask(() -> {
|
||||
saveThread.set(Thread.currentThread());
|
||||
serialized.incrementAndGet();
|
||||
compression.queueTask(() -> {
|
||||
compressionThread.set(Thread.currentThread());
|
||||
compressed.incrementAndGet();
|
||||
pendingWrite.set(false);
|
||||
});
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
|
||||
|
||||
assertEquals(1L, releaseGeneration.getCount());
|
||||
assertEquals(1, serialized.get());
|
||||
assertEquals(1, compressed.get());
|
||||
assertFalse(pendingWrite.get());
|
||||
assertSame(Thread.currentThread(), saveThread.get());
|
||||
assertSame(Thread.currentThread(), compressionThread.get());
|
||||
} finally {
|
||||
releaseGeneration.countDown();
|
||||
pool.halt(true);
|
||||
assertTrue(pool.joinInterruptable(5_000));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkPos chunk = new ChunkPos(2, 3);
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
|
||||
assertTrue(failure.getMessage().contains("Timed out"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkPos chunk = new ChunkPos(2, 3);
|
||||
Thread.currentThread().interrupt();
|
||||
try {
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||
assertTrue(failure.getMessage().contains("Interrupted"));
|
||||
assertTrue(Thread.currentThread().isInterrupted());
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
|
||||
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
|
||||
savesField.setAccessible(true);
|
||||
compressionField.setAccessible(true);
|
||||
savesField.set(scheduler, saves);
|
||||
compressionField.set(scheduler, compression);
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
return level;
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import com.mojang.serialization.Codec;
|
||||
@@ -64,6 +64,21 @@ public class WorldgenTerrainHeightmapsTest {
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP + 6,
|
||||
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
|
||||
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
+3
-3
@@ -16,10 +16,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.structure;
|
||||
package art.arcane.iris.structure;
|
||||
|
||||
import art.arcane.iris.core.structure.authoring.StructureCapability;
|
||||
import art.arcane.iris.core.structure.authoring.StructureLoss;
|
||||
import art.arcane.iris.structure.authoring.StructureCapability;
|
||||
import art.arcane.iris.structure.authoring.StructureLoss;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
@@ -21,8 +21,8 @@ dependencies {
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(libs.sentry)
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(testFixtures(project(':core')))
|
||||
testImplementation(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
@@ -60,18 +60,4 @@ tasks.named('jar', Jar).configure {
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
systemProperty('iris.commandFindSource', file('src/main/java/art/arcane/iris/core/commands/CommandFind.java').absolutePath)
|
||||
systemProperty('iris.commandStructureSource', file('src/main/java/art/arcane/iris/core/commands/CommandStructure.java').absolutePath)
|
||||
systemProperty('iris.commandIrisSource', file('src/main/java/art/arcane/iris/core/commands/CommandIris.java').absolutePath)
|
||||
systemProperty('iris.bukkitChunkGeneratorSource', rootProject.file('core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java').absolutePath)
|
||||
systemProperty('iris.pregeneratorJobSource', rootProject.file('core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java').absolutePath)
|
||||
systemProperty('iris.bukkitEnginePlatformHooksSource', file('src/main/java/art/arcane/iris/core/runtime/BukkitEnginePlatformHooks.java').absolutePath)
|
||||
systemProperty('iris.engineSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java').absolutePath)
|
||||
systemProperty('iris.jigsawStudioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/JigsawStudioService.java').absolutePath)
|
||||
systemProperty('iris.studioSvcSource', rootProject.file('core/src/main/java/art/arcane/iris/core/service/StudioSVC.java').absolutePath)
|
||||
systemProperty('iris.terrainSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java').absolutePath)
|
||||
systemProperty('iris.apiEventSvcSource', file('src/main/java/art/arcane/iris/core/service/IrisApiEventSVC.java').absolutePath)
|
||||
systemProperty('iris.worldInfoFactorySource', file('src/main/java/art/arcane/iris/core/service/terrain/IrisWorldInfoFactory.java').absolutePath)
|
||||
systemProperty('iris.startupSource', file('src/main/java/art/arcane/iris/Iris.java').absolutePath)
|
||||
systemProperty('iris.readmeSource', rootProject.file('README.md').absolutePath)
|
||||
}
|
||||
|
||||
@@ -18,72 +18,78 @@
|
||||
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineEffects;
|
||||
import art.arcane.iris.engine.IrisWorldManager;
|
||||
import art.arcane.volmlib.util.diagnostics.BukkitDebugDump;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineEffects;
|
||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
||||
|
||||
import art.arcane.iris.engine.framework.EngineComponentCleanup;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisStartupValidation;
|
||||
import art.arcane.iris.core.IrisStartupAdmissionListener;
|
||||
import art.arcane.iris.core.BukkitWorldReconciler;
|
||||
import art.arcane.iris.core.IrisWorldGeneratorResolver;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.PendingWorldDeleteQueue;
|
||||
import art.arcane.iris.core.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.core.SettingsHotloadWatch;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||
import art.arcane.iris.core.lifecycle.ManagedWorldLoader;
|
||||
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
|
||||
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.core.runtime.WorldDeletionQueue;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.generation.runtime.EngineComponentCleanup;
|
||||
import art.arcane.iris.generation.runtime.EngineEffectsProvider;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.IrisStartupValidation;
|
||||
import art.arcane.iris.world.IrisStartupAdmissionListener;
|
||||
import art.arcane.iris.world.BukkitWorldReconciler;
|
||||
import art.arcane.iris.world.IrisWorldGeneratorResolver;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.PendingWorldDeleteQueue;
|
||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.configuration.SettingsHotloadWatch;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
|
||||
import art.arcane.iris.world.lifecycle.ManagedWorldLoader;
|
||||
import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
|
||||
import art.arcane.iris.world.lifecycle.PaperLibBootstrap;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||
import art.arcane.iris.world.runtime.WorldDeletionQueue;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import art.arcane.iris.core.link.IrisPapiInstaller;
|
||||
import art.arcane.iris.core.link.IrisPapiListener;
|
||||
import art.arcane.iris.core.link.IrisPapiState;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.link.MultiverseCoreLink;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.service.BoardSVC;
|
||||
import art.arcane.iris.core.service.CommandSVC;
|
||||
import art.arcane.iris.core.service.DatapackStructureScopeSVC;
|
||||
import art.arcane.iris.core.service.EditSVC;
|
||||
import art.arcane.iris.core.service.EntityRiseSVC;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.core.service.GlobalCacheSVC;
|
||||
import art.arcane.iris.core.service.IrisApiEventSVC;
|
||||
import art.arcane.iris.core.service.IrisEngineSVC;
|
||||
import art.arcane.iris.core.service.IrisIntegrationService;
|
||||
import art.arcane.iris.core.service.IrisProtocolService;
|
||||
import art.arcane.iris.core.service.IrisTerrainSVC;
|
||||
import art.arcane.iris.core.service.JigsawStudioService;
|
||||
import art.arcane.iris.core.service.LogFilterSVC;
|
||||
import art.arcane.iris.core.service.MultiverseSVC;
|
||||
import art.arcane.iris.core.service.ObjectSVC;
|
||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||
import art.arcane.iris.core.service.PreservationSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.service.TreeFellerSVC;
|
||||
import art.arcane.iris.core.service.TreeSVC;
|
||||
import art.arcane.iris.core.service.WandSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.EnginePanic;
|
||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.object.IrisCompat;
|
||||
import art.arcane.iris.core.safeguard.IrisSafeguard;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.integration.IrisPapiInstaller;
|
||||
import art.arcane.iris.integration.IrisPapiListener;
|
||||
import art.arcane.iris.integration.IrisPapiState;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||
import art.arcane.iris.integration.MultiverseCoreLink;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||
import art.arcane.iris.command.CommandSVC;
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeSVC;
|
||||
import art.arcane.iris.studio.edit.EditSVC;
|
||||
import art.arcane.iris.world.entity.EntityRiseSVC;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.generation.runtime.GlobalCacheSVC;
|
||||
import art.arcane.iris.platform.bukkit.api.IrisApiEventSVC;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineSVC;
|
||||
import art.arcane.iris.integration.IrisIntegrationService;
|
||||
import art.arcane.iris.platform.protocol.IrisProtocolService;
|
||||
import art.arcane.iris.platform.bukkit.api.IrisTerrainSVC;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.diagnostics.LogFilterSVC;
|
||||
import art.arcane.iris.integration.MultiverseSVC;
|
||||
import art.arcane.iris.studio.object.ObjectSVC;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
import art.arcane.iris.generation.runtime.PreservationSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.world.tree.TreeFellerSVC;
|
||||
import art.arcane.iris.world.tree.TreeSVC;
|
||||
import art.arcane.iris.studio.wand.WandSVC;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.EnginePanic;
|
||||
import art.arcane.iris.generation.runtime.BlockEditAccess;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.pack.validation.IrisCompat;
|
||||
import art.arcane.iris.world.safeguard.IrisSafeguard;
|
||||
import art.arcane.iris.world.safeguard.RuntimeLockNotice;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -94,7 +100,7 @@ import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.function.NastyRunnable;
|
||||
import art.arcane.volmlib.util.hud.HudActionBar;
|
||||
import art.arcane.volmlib.util.hud.HudBossBarLane;
|
||||
@@ -105,16 +111,16 @@ import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.plugin.ComponentLog;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import art.arcane.volmlib.util.plugin.ComponentText;
|
||||
import art.arcane.iris.util.common.misc.Bindings;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.common.misc.SlimJar;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.plugin.chunk.ChunkTickets;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.simd.SimdSupport;
|
||||
import art.arcane.iris.platform.bootstrap.Bindings;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.platform.bukkit.chunk.ChunkTickets;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.generation.simd.SimdSupport;
|
||||
import art.arcane.volmlib.util.scheduling.Queue;
|
||||
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -136,11 +142,14 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -153,10 +162,12 @@ import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
|
||||
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
|
||||
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
|
||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||
|
||||
@@ -187,6 +198,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private volatile boolean generatorDrainCompleted;
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
@@ -199,20 +212,30 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
|
||||
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
|
||||
private final PendingWorldReplacementManager pendingWorldReplacements = new PendingWorldReplacementManager(this);
|
||||
private BukkitLanguageSwitcher languageSwitcher;
|
||||
private BukkitDebugDump debugDump;
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile Thread serverLifecycleThread;
|
||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
if (sender == null) {
|
||||
sender = new VolmitSender(Bukkit.getConsoleSender());
|
||||
sender.setTag(instance.getTag());
|
||||
VolmitSender current = sender;
|
||||
if (current == null) {
|
||||
Iris plugin = instance;
|
||||
current = new VolmitSender(Bukkit.getConsoleSender());
|
||||
current.setTag(plugin == null ? IrisSafeguard.mode().tag("") : plugin.getTag());
|
||||
sender = current;
|
||||
}
|
||||
return sender;
|
||||
return current;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T service(Class<T> c) {
|
||||
return (T) instance.services.get(c);
|
||||
Iris plugin = instance;
|
||||
if (plugin == null || plugin.services == null) {
|
||||
throw new IllegalStateException("Iris is disabled; " + c.getSimpleName() + " is unavailable");
|
||||
}
|
||||
return (T) plugin.services.get(c);
|
||||
}
|
||||
|
||||
public static void callEvent(Event e) {
|
||||
@@ -251,7 +274,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public static File getTemp() {
|
||||
return instance.getDataFolder("cache", "temp");
|
||||
Iris plugin = instance;
|
||||
if (plugin == null) {
|
||||
throw new IllegalStateException("Iris is disabled; the temp folder is unavailable");
|
||||
}
|
||||
return plugin.getDataFolder("cache", "temp");
|
||||
}
|
||||
|
||||
public static void msg(String string) {
|
||||
@@ -293,10 +320,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
|
||||
StackWalker.StackFrame frame = null;
|
||||
StackWalker.StackFrame frame;
|
||||
try {
|
||||
frame = DEBUG_STACK_WALKER.walk(stream -> stream.skip(1).findFirst().orElse(null));
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable unavailable) {
|
||||
frame = null;
|
||||
}
|
||||
|
||||
if (frame == null) {
|
||||
@@ -422,14 +450,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (e == null) {
|
||||
return;
|
||||
}
|
||||
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return;
|
||||
}
|
||||
|
||||
Bindings.capture(e);
|
||||
boolean debug = false;
|
||||
if (instance != null) {
|
||||
try {
|
||||
IrisSettings currentSettings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get();
|
||||
debug = currentSettings != null && currentSettings.getGeneral().isDebug();
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable unreadable) {
|
||||
debug = false;
|
||||
}
|
||||
}
|
||||
@@ -455,11 +486,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
Iris.debug("Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage());
|
||||
}
|
||||
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
|
||||
public static void reportError(String context, Throwable e) {
|
||||
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
|
||||
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
|
||||
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] " + message);
|
||||
error.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance != null) {
|
||||
@@ -473,7 +511,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
reportError(error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
|
||||
public static void dump() {
|
||||
@@ -520,11 +557,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
Iris::msg,
|
||||
Iris::reportError,
|
||||
(event) -> Iris.callEvent((org.bukkit.event.Event) event),
|
||||
() -> Iris.instance.getDataFolder(),
|
||||
(path) -> Iris.instance.getDataFile(path),
|
||||
() -> Iris.instance.getJarFile(),
|
||||
() -> Iris.instance.getIrisVersion(),
|
||||
() -> Iris.instance.getMCVersion()));
|
||||
this::getDataFolder,
|
||||
this::getDataFile,
|
||||
this::getJarFile,
|
||||
this::getIrisVersion,
|
||||
this::getMCVersion));
|
||||
SlimJar.load();
|
||||
}
|
||||
|
||||
@@ -591,6 +628,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
||||
return false;
|
||||
}
|
||||
EnableTimings timings = new EnableTimings();
|
||||
alreadyDrained.set(false);
|
||||
postStopFinisherStarted.set(false);
|
||||
serverStopTeardownDeferred.set(false);
|
||||
@@ -598,15 +636,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
sharedRuntimeClosed.set(false);
|
||||
startupBoundaryRestart.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
generatorDrainCompleted = false;
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.hydrology.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
IrisLanguage.initialize();
|
||||
debugDump = BukkitDebugDump.create(this, new BukkitDebugDump.Options(
|
||||
() -> true,
|
||||
() -> IrisSafeguard::debugReport));
|
||||
languageSwitcher = BukkitLanguageSwitcher.register(this, IrisLanguage.selections(),
|
||||
new BukkitLanguageSwitcher.Options("iris", "iris.all",
|
||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||
PaperLibBootstrap.install();
|
||||
SimdSupport.install();
|
||||
timings.mark("bootstrap");
|
||||
services = new KMap<>();
|
||||
BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
|
||||
Bindings.setupSentry();
|
||||
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
|
||||
// enable/disable and paid a full-jar class sweep at boot. Infrastructure first,
|
||||
// engine/world services next, commands last.
|
||||
@@ -643,12 +690,13 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||
IrisServices.register(IrisCompat.class, compat);
|
||||
timings.mark("services");
|
||||
ServerConfigurator.configure();
|
||||
timings.mark("serverConfig");
|
||||
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
||||
if (datapackValidation == StartupValidationOutcome.READY) {
|
||||
generatorResolver.validateAllPacks();
|
||||
}
|
||||
timings.mark("datapacks");
|
||||
IrisSafeguard.execute();
|
||||
timings.mark("safeguard");
|
||||
getSender().setTag(getTag());
|
||||
// A cosmetic banner must never abort the bootstrap.
|
||||
J.attempt(this::splash);
|
||||
@@ -657,6 +705,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
// Paper's bootstrap runs before any plugin logger exists, so orphan-storage warnings raised there
|
||||
// never reach logs/latest.log. Replay them once now that the platform log is up.
|
||||
MissingWorldStorageLog.replayToPlatformLog();
|
||||
timings.mark("splash");
|
||||
tickets = new ChunkTickets();
|
||||
linkMultiverseCore = new MultiverseCoreLink();
|
||||
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
|
||||
@@ -669,18 +718,21 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
|
||||
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("iris.json"));
|
||||
settingsHotloadWatch = watch;
|
||||
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
|
||||
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
||||
IO.delete(getTemp());
|
||||
StudioSVC.gateDownloadsOnStaleTempCleanup(MultiBurst.ioBurst.completeValueAsync(() -> {
|
||||
IO.delete(getTemp());
|
||||
return null;
|
||||
}));
|
||||
timings.mark("tempSweep");
|
||||
// One throwing service must not abort the bootstrap: the steps after this loop
|
||||
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
|
||||
// Only services that actually enabled get listeners and a later onDisable.
|
||||
enabledServices.clear();
|
||||
for (IrisService service : orderedServices) {
|
||||
long serviceStartedAt = System.nanoTime();
|
||||
try {
|
||||
service.onEnable();
|
||||
enabledServices.add(service);
|
||||
timings.markService(service.getClass().getSimpleName(), serviceStartedAt);
|
||||
} catch (Throwable e) {
|
||||
// A service failure is NOT a datapack validation failure: the admission gate
|
||||
// must never lock every login over a broken cosmetic service. Log loudly,
|
||||
@@ -701,12 +753,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
|
||||
}
|
||||
}
|
||||
timings.mark("serviceEnable");
|
||||
if (datapackValidation == StartupValidationOutcome.READY) {
|
||||
IrisServices.get(ExternalDataSVC.class).setContentChangeListener(generatorResolver::requestExternalContentRefresh);
|
||||
generatorResolver.validateAllPacks();
|
||||
}
|
||||
timings.mark("packValidation");
|
||||
addShutdownHook();
|
||||
pendingWorldReplacements.processPendingStartupReplacements();
|
||||
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
||||
|
||||
if (J.isFolia() && IrisStartupValidation.isReady()) {
|
||||
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
|
||||
J.s(this::reconcileStartupWorlds, 1);
|
||||
}
|
||||
|
||||
J.s(() -> {
|
||||
@@ -721,19 +779,67 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
autoStartStudio();
|
||||
}
|
||||
if (!J.isFolia() && IrisStartupValidation.isReady()) {
|
||||
worldReconciler.checkForBukkitWorlds(s -> true);
|
||||
reconcileStartupWorlds();
|
||||
}
|
||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||
// The mantle stores block values as PlatformBlockState, so a BlockData retention can never
|
||||
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
|
||||
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
||||
});
|
||||
timings.mark("startupTasks");
|
||||
timings.report();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final class EnableTimings {
|
||||
private static final int REPORTED_SERVICES = 3;
|
||||
|
||||
private final long startedAt = System.nanoTime();
|
||||
private final StringBuilder phases = new StringBuilder();
|
||||
private final Map<String, Long> serviceMillis = new LinkedHashMap<>();
|
||||
private long lastMark = startedAt;
|
||||
|
||||
private void mark(String phase) {
|
||||
long now = System.nanoTime();
|
||||
if (!phases.isEmpty()) {
|
||||
phases.append(' ');
|
||||
}
|
||||
phases.append(phase).append('=').append(TimeUnit.NANOSECONDS.toMillis(now - lastMark)).append("ms");
|
||||
lastMark = now;
|
||||
}
|
||||
|
||||
private void markService(String service, long serviceStartedAt) {
|
||||
serviceMillis.put(service, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - serviceStartedAt));
|
||||
}
|
||||
|
||||
private void report() {
|
||||
IrisLogging.notice("Enabled in %dms (%s)",
|
||||
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt), phases);
|
||||
String slowest = serviceMillis.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
||||
.limit(REPORTED_SERVICES)
|
||||
.map(entry -> entry.getKey() + "=" + entry.getValue() + "ms")
|
||||
.collect(Collectors.joining(" "));
|
||||
if (!slowest.isBlank()) {
|
||||
IrisLogging.notice("Slowest services: %s", slowest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reconcileStartupWorlds() {
|
||||
generatorResolver.startupWorldsReady().whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Iris.reportError("Could not resume Iris startup world reconciliation.", failure);
|
||||
return;
|
||||
}
|
||||
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true));
|
||||
});
|
||||
}
|
||||
|
||||
public void addShutdownHook() {
|
||||
removeShutdownHook();
|
||||
serverLifecycleThread = Thread.currentThread();
|
||||
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||
try {
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
@@ -828,6 +934,25 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
.orElse("Iris startup validation requires a restart.");
|
||||
startupBoundaryRestart.set(true);
|
||||
ServerConfigurator.restartAtStartupBoundary(restartReason);
|
||||
return;
|
||||
}
|
||||
reportLockedRuntime();
|
||||
}
|
||||
|
||||
private static void reportLockedRuntime() {
|
||||
String denial = IrisStartupValidation.denialReason().orElse(null);
|
||||
if (denial == null) {
|
||||
return;
|
||||
}
|
||||
boolean managedStorage;
|
||||
try {
|
||||
managedStorage = IrisWorldStorage.hasManagedWorldStorage(IrisWorldStorage.levelRoot());
|
||||
} catch (Throwable unavailable) {
|
||||
Iris.reportError("Could not inspect Iris world storage while reporting the locked runtime.", unavailable);
|
||||
managedStorage = false;
|
||||
}
|
||||
for (String line : RuntimeLockNotice.compose(denial, managedStorage)) {
|
||||
Iris.error(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,7 +987,32 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
|
||||
public BukkitDebugDump debugDump() {
|
||||
return debugDump;
|
||||
}
|
||||
|
||||
public void selectLanguage(CommandSender sender, String[] arguments) {
|
||||
BukkitLanguageSwitcher current = languageSwitcher;
|
||||
if (current != null) {
|
||||
current.command(sender, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> completeLanguage(CommandSender sender, String[] arguments) {
|
||||
BukkitLanguageSwitcher current = languageSwitcher;
|
||||
return current == null ? List.of() : current.complete(sender, arguments);
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
if (debugDump != null) {
|
||||
debugDump.close();
|
||||
debugDump = null;
|
||||
}
|
||||
if (languageSwitcher != null) {
|
||||
languageSwitcher.close();
|
||||
languageSwitcher = null;
|
||||
}
|
||||
IrisLanguage.shutdown();
|
||||
teardownPapi();
|
||||
boolean serverStopping = IrisToolbelt.isServerStopping();
|
||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||
@@ -930,6 +1080,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
drainWorldGenerators(reason, timeoutSeconds);
|
||||
}
|
||||
|
||||
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
||||
// Only services whose onEnable actually completed; disabling a service that
|
||||
// never initialized runs teardown against uninitialized state.
|
||||
@@ -937,6 +1091,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
|
||||
}
|
||||
}
|
||||
@@ -946,15 +1101,41 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
|
||||
J.attempt(MultiBurst.burst::close);
|
||||
J.attempt(MultiBurst.ioBurst::close);
|
||||
try {
|
||||
MultiBurst.burst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.hydrology.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.ioBurst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||
}
|
||||
clearQueues();
|
||||
IrisServices.clear();
|
||||
IrisRuntimeStatics.reset();
|
||||
releaseStatics();
|
||||
}
|
||||
}
|
||||
|
||||
private static void releaseStatics() {
|
||||
linkMultiverseCore = null;
|
||||
compat = null;
|
||||
tickets = null;
|
||||
sender = null;
|
||||
instance = null;
|
||||
}
|
||||
|
||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||
serverStopTeardownDeferred.set(true);
|
||||
INMS.get().deferPluginClassLoaderClose();
|
||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||
if (jigsawStudioService != null) {
|
||||
try {
|
||||
@@ -969,7 +1150,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
try {
|
||||
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS);
|
||||
if (J.isFolia()) {
|
||||
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS);
|
||||
} else {
|
||||
List<World> worlds = Bukkit.getWorlds();
|
||||
PregeneratorJob.shutdownAndWait(SERVER_STOP_PREGEN_TIMEOUT_MILLIS, () -> {
|
||||
for (World world : worlds) {
|
||||
INMS.get().pollChunkTask(world);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to quiesce the Iris pregenerator before server shutdown.", e);
|
||||
}
|
||||
@@ -1027,29 +1217,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
if (!awaitServerShutdownBoundary()) {
|
||||
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
|
||||
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
||||
}
|
||||
|
||||
private boolean awaitServerShutdownBoundary() {
|
||||
if (!INMS.isBound()) {
|
||||
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||
if (boundary == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return INMS.get().awaitServerShutdownBoundary(
|
||||
return boundary.await(
|
||||
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
|
||||
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
|
||||
teardownRuntime(reason, timeoutSeconds);
|
||||
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
|
||||
return;
|
||||
}
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
|
||||
@@ -1061,12 +1257,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
runPostShutdown();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||
} finally {
|
||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
||||
}
|
||||
IrisPlatforms.unbind();
|
||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
||||
} else {
|
||||
try {
|
||||
INMS.get().releasePluginClassLoaderClose();
|
||||
} catch (Throwable failure) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||
}
|
||||
}
|
||||
BukkitPlatform.releaseHost();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasActiveShutdownResources() {
|
||||
if (!MultiBurst.burst.isTerminated() || !MultiBurst.hydrology.isTerminated()
|
||||
|| !MultiBurst.ioBurst.isTerminated()) {
|
||||
return true;
|
||||
}
|
||||
return services != null
|
||||
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
|
||||
&& preservation.hasActiveResources();
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||
@@ -1082,6 +1304,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
if (generators.isEmpty()) {
|
||||
generatorDrainCompleted = true;
|
||||
Iris.debug("No Iris worlds to freeze.");
|
||||
return;
|
||||
}
|
||||
@@ -1106,12 +1329,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
|
||||
.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
||||
generatorDrainCompleted = closes.size() == generators.size();
|
||||
if (generatorDrainCompleted) {
|
||||
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
|
||||
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Iris.warn("Iris generator drain interrupted; unload proceeding.");
|
||||
Iris.reportError("Iris generator drain was interrupted.", e);
|
||||
} catch (ExecutionException e) {
|
||||
Iris.reportError(e.getCause() == null ? e : e.getCause());
|
||||
}
|
||||
@@ -1245,7 +1471,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public int getIrisVersion() {
|
||||
String input = Iris.instance.getDescription().getVersion();
|
||||
String input = getDescription().getVersion();
|
||||
int hyphenIndex = input.indexOf('-');
|
||||
if (hyphenIndex != -1) {
|
||||
String result = input.substring(0, hyphenIndex);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
|
||||
import art.arcane.iris.core.lifecycle.HuskWorldQuarantine;
|
||||
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import art.arcane.iris.util.common.misc.SlimJar;
|
||||
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
|
||||
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
|
||||
import art.arcane.iris.world.lifecycle.HuskWorldQuarantine;
|
||||
import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
|
||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
|
||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
|
||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.util.common.misc.SlimJar;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
|
||||
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record IrisBiomeInfo(
|
||||
String key,
|
||||
String name,
|
||||
String regionKey,
|
||||
String regionName,
|
||||
String derivativeKey,
|
||||
String vanillaDerivativeKey,
|
||||
String type,
|
||||
List<IrisCustomBiomeInfo> customDerivatives) {
|
||||
public IrisBiomeInfo {
|
||||
key = key == null ? "" : key;
|
||||
name = name == null ? "" : name;
|
||||
regionKey = regionKey == null ? "" : regionKey;
|
||||
regionName = regionName == null ? "" : regionName;
|
||||
derivativeKey = derivativeKey == null ? "" : derivativeKey;
|
||||
vanillaDerivativeKey = vanillaDerivativeKey == null ? "" : vanillaDerivativeKey;
|
||||
type = type == null ? "" : type;
|
||||
customDerivatives = List.copyOf(customDerivatives);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -2,6 +2,11 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisColumnField {
|
||||
SURFACE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
SURFACE_KIND,
|
||||
BIOME_KEY
|
||||
BIOME_KEY,
|
||||
RIVER_STATE,
|
||||
RIVER_DISTANCE,
|
||||
RIVER_FLOW,
|
||||
RIVER_WATER_SURFACE_Y
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnSample(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
|
||||
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
|
||||
public static final int UNAVAILABLE_RIVER_FLOW = -1;
|
||||
|
||||
public IrisColumnSample {
|
||||
Objects.requireNonNull(surfaceKind, "surfaceKind");
|
||||
Objects.requireNonNull(riverState, "riverState");
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
|
||||
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
|
||||
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
|
||||
}
|
||||
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
|
||||
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSurfaceHeight() {
|
||||
return surfaceHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasNaturalHeight() {
|
||||
return naturalHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasSurfaceKind() {
|
||||
return surfaceKind != IrisSurfaceKind.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasBiomeKey() {
|
||||
return biomeKey != null;
|
||||
}
|
||||
|
||||
public boolean hasRiverState() {
|
||||
return riverState != IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
public boolean hasRiverDistance() {
|
||||
return !Double.isNaN(riverDistance);
|
||||
}
|
||||
|
||||
public boolean hasRiverFlow() {
|
||||
return riverFlow != UNAVAILABLE_RIVER_FLOW;
|
||||
}
|
||||
|
||||
public boolean hasRiverWaterSurfaceY() {
|
||||
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisColumnSink {
|
||||
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
|
||||
void accept(IrisColumnSample sample);
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public record IrisCustomBiomeInfo(String id, String registryKey) {
|
||||
public IrisCustomBiomeInfo {
|
||||
id = id == null ? "" : id;
|
||||
registryKey = registryKey == null ? "" : registryKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisRiverState {
|
||||
NONE,
|
||||
WET,
|
||||
DRY
|
||||
}
|
||||
@@ -4,6 +4,9 @@ public enum IrisSurfaceKind {
|
||||
UNKNOWN,
|
||||
LAND,
|
||||
SHORE,
|
||||
RIVER,
|
||||
RIVER_SHORE,
|
||||
DRY_CHANNEL,
|
||||
OCEAN,
|
||||
VOID
|
||||
}
|
||||
|
||||
+2
@@ -18,6 +18,8 @@ public interface IrisTerrainService {
|
||||
|
||||
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
|
||||
|
||||
Optional<String> regionKey(World world, int blockX, int blockZ);
|
||||
|
||||
+8
-9
@@ -16,23 +16,22 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
||||
public class CommandDatapack implements DirectorExecutor {
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
||||
public void ingest(
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.command.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
@Director(name = "debug", description = "Iris diagnostic tools", descriptionKey = "iris.director.commanddebug.description")
|
||||
public final class CommandDebug implements DirectorExecutor {
|
||||
@Director(name = "dump", sync = true, description = "Create and optionally upload a diagnostic report", descriptionKey = "iris.director.commandiris.director.debugdump")
|
||||
public void dump(
|
||||
@Param(name = "upload", defaultValue = "true", description = "Upload the report to mclo.gs", descriptionKey = "iris.director.commandiris.param.debugdump_upload") boolean upload,
|
||||
@Param(name = "sender", contextual = true) CommandSender sender
|
||||
) {
|
||||
Iris.instance.debugDump().request(sender, upload);
|
||||
}
|
||||
|
||||
@Director(sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
|
||||
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
|
||||
ComponentMessenger.sendMarkup(sender, DirectorMiniMenu.version(
|
||||
"Iris", Iris.instance.getDescription().getVersion(), DirectorMiniMenu.Theme.irisGreen()));
|
||||
}
|
||||
|
||||
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
|
||||
public void toggle() {
|
||||
boolean enabled = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(enabled);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG,
|
||||
MessageArgument.untrusted("to", enabled)));
|
||||
}
|
||||
}
|
||||
+27
-42
@@ -16,40 +16,38 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.runtime.ChunkClearer;
|
||||
import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
|
||||
import art.arcane.iris.core.service.IrisEngineSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterContext;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.world.runtime.ChunkClearer;
|
||||
import art.arcane.iris.generation.validation.GoldenHashScanner;
|
||||
import art.arcane.iris.world.runtime.InPlaceChunkRegenerator;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.pack.IrisPackBenchmarking;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterContext;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.CountingDataInputStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAFile;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.world.storage.region.MCAFile;
|
||||
import art.arcane.iris.world.storage.region.MCAUtil;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -66,11 +64,11 @@ import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", descriptionKey = "iris.director.commanddeveloper.director.iris_world_manager", aliases = {"dev"})
|
||||
public class CommandDeveloper implements DirectorExecutor {
|
||||
@Director(description = "Get Loaded TectonicPlates Count", descriptionKey = "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", origin = DirectorOrigin.BOTH, sync = true)
|
||||
@@ -79,19 +77,6 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
.engineStatus(sender());
|
||||
}
|
||||
|
||||
@Director(description = "Send a test exception to sentry", descriptionKey = "iris.director.commanddeveloper.director.send_test_exception_sentry")
|
||||
public void Sentry() {
|
||||
Engine engine = engine();
|
||||
Exception testException = new Exception("This is a test");
|
||||
if (engine == null) {
|
||||
Iris.reportError(testException);
|
||||
return;
|
||||
}
|
||||
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
|
||||
Iris.reportError(testException);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", descriptionKey = "iris.director.commanddeveloper.director.hash_generated_block_output_fixed_area_determinism_identity_testing", origin = DirectorOrigin.BOTH)
|
||||
public void genhash(
|
||||
@Param(description = "The world to hash", descriptionKey = "iris.director.commanddeveloper.param.world_hash", contextual = true, contextualOverride = true)
|
||||
@@ -190,7 +175,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
return z ^ (z >>> 31);
|
||||
}
|
||||
|
||||
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
|
||||
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@@ -216,7 +201,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
|
||||
pack.getLoadKey()
|
||||
)) {
|
||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
|
||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||
}
|
||||
+8
-9
@@ -16,25 +16,24 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something", descriptionKey = "iris.director.commandedit.director.edit_something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
+166
-41
@@ -16,32 +16,38 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.command.specialhandlers.HydrologyTypeHandler;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.command.specialhandlers.LocatableObjectHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ReachableRegionHandler;
|
||||
import art.arcane.iris.command.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
@@ -68,7 +74,7 @@ import java.util.Set;
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look")
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -85,7 +91,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -116,6 +122,75 @@ public class CommandFind implements DirectorExecutor {
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine activeEngine = engine();
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = player();
|
||||
if (activeEngine == null || commandSender == null || target == null) {
|
||||
if (commandSender != null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
World originWorld = origin.getWorld();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||
activeEngine,
|
||||
query,
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
originWorld, feature.x(), feature.y(), feature.z());
|
||||
prepareStructureTeleport(
|
||||
target, originWorld, commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
|
||||
@@ -159,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
|
||||
registered, decision, nativePlacement, locatableNativePlacement,
|
||||
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
||||
|
||||
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
|
||||
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (route == StructureLookupRoute.UNKNOWN) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
||||
return;
|
||||
@@ -334,7 +415,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
|
||||
Set<String> configuredImportUrls = normalizedImportUrls(
|
||||
engine.getDimension().getDatapackImports());
|
||||
DatapackIngestService.configuredImports(engine.getDimension()));
|
||||
return new StructureExclusionReport(
|
||||
List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()),
|
||||
configuredImportUrls, registeredExcluded, configuredUnregistered,
|
||||
@@ -598,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
||||
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
@@ -623,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
|
||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -647,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
if (e.hasObjectPlacement(object)) {
|
||||
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
|
||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
@@ -657,22 +738,41 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
J.runEntity(target, () -> {
|
||||
if (!target.isOnline()) {
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
if (!world.equals(target.getWorld())) {
|
||||
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
|
||||
return;
|
||||
}
|
||||
loadStructureDestination(target, world, commandSender, structure, at, useLocatedY);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadStructureDestination(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
try {
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
}
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
try {
|
||||
@@ -680,16 +780,41 @@ public class CommandFind implements DirectorExecutor {
|
||||
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
|
||||
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
J.runEntity(target, () -> {
|
||||
BukkitPlatform.teleportAsync(target, destination);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", at.getBlockX()), MessageArgument.untrusted("y", y), MessageArgument.untrusted("value2", at.getBlockZ())));
|
||||
});
|
||||
J.runEntity(target, () -> completeStructureTeleport(target, world, commandSender, structure, destination));
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
|
||||
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location destination) {
|
||||
if (!target.isOnline()) {
|
||||
return;
|
||||
}
|
||||
if (!world.equals(target.getWorld())) {
|
||||
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BukkitPlatform.teleportAsync(target, destination).whenComplete((teleported, error) -> {
|
||||
if (error != null) {
|
||||
Iris.reportError("Could not teleport to '" + structure + "'.", error);
|
||||
}
|
||||
sendStructureMessage(target, commandSender, error == null && Boolean.TRUE.equals(teleported)
|
||||
? IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED,
|
||||
MessageArgument.untrusted("structure", structure),
|
||||
MessageArgument.untrusted("value", destination.getBlockX()),
|
||||
MessageArgument.untrusted("y", destination.getBlockY()),
|
||||
MessageArgument.untrusted("value2", destination.getBlockZ()))
|
||||
: C.RED + "Could not teleport to " + structure + ".");
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
commandSender.sendMessage(C.RED + "Could not teleport to " + structure + ".");
|
||||
Iris.reportError("Could not teleport to '" + structure + "'.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||
}
|
||||
+41
-41
@@ -16,40 +16,40 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.BukkitWorldReconciler;
|
||||
import art.arcane.iris.world.BukkitWorldReconciler;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.lifecycle.IrisWorldRemovalService;
|
||||
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.IrisWorlds;
|
||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.world.lifecycle.IrisWorldRemovalService;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.PackDownloader;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.command.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
@@ -67,15 +67,15 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||
import static art.arcane.iris.platform.bootstrap.ServerProperties.BUKKIT_YML;
|
||||
import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.IrisMessages;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.iris.localization.PackDownloadMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
private static final String NO_DOWNLOAD_SOURCE = "__none__";
|
||||
@@ -93,8 +93,14 @@ public class CommandIris implements DirectorExecutor {
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
private CommandDatapack datapack;
|
||||
private CommandDebug debug;
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Director(description = "Choose your language or the server default")
|
||||
public void language() {
|
||||
Iris.instance.selectLanguage(sender().getS(), new String[0]);
|
||||
}
|
||||
|
||||
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
|
||||
public void create(
|
||||
@Param(aliases = "world-name", description = "The name of the world to create", descriptionKey = "iris.director.commandiris.param.name_world_create")
|
||||
@@ -243,6 +249,8 @@ public class CommandIris implements DirectorExecutor {
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
|
||||
+ "verifies the replacement.");
|
||||
} catch (LifecycleOperationCoordinator.BusyException busy) {
|
||||
sender().sendMessage(C.YELLOW + busy.getMessage());
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
@@ -298,9 +306,9 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
|
||||
public void version() {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
|
||||
@Director(hidden = true, sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
|
||||
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
|
||||
debug.version(sender);
|
||||
}
|
||||
|
||||
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
|
||||
@@ -424,14 +432,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
return failure.getMessage();
|
||||
}
|
||||
|
||||
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
|
||||
public void debug() {
|
||||
boolean to = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(to);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG, MessageArgument.untrusted("to", to)));
|
||||
}
|
||||
|
||||
@Director(description = "Download a project.", descriptionKey = "iris.director.commandiris.director.download_project", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The built-in pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", defaultValue = NO_DOWNLOAD_SOURCE, customHandler = DownloadPackHandler.class)
|
||||
@@ -463,7 +463,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SENDING_METRICS));
|
||||
engine().printMetrics(sender());
|
||||
engine().printMetrics(sender()::sendMessage);
|
||||
}
|
||||
|
||||
@Director(description = "Reload configuration file (this is also done automatically)", descriptionKey = "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically")
|
||||
@@ -0,0 +1,815 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawPlanarArchetype;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioProjectCreator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEvaluation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.structure.VillageImporter;
|
||||
import art.arcane.iris.structure.authoring.StructureKey;
|
||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawCompatibility;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawThemeSet;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawWorkcellArchetype;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.command.specialhandlers.IrisStructureHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Director(name = "jigsaw", aliases = {"jig", "jgs"}, description = "Iris Jigsaw Studio tools",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsaw implements DirectorExecutor {
|
||||
static final long DEFAULT_STUDIO_SEED = 1337L;
|
||||
static final StudioOpenCoordinator.StudioOpenKind STUDIO_OPEN_KIND =
|
||||
StudioOpenCoordinator.StudioOpenKind.JIGSAW;
|
||||
static final Map<UUID, SessionBinding> PLAYER_PACKS = new ConcurrentHashMap<>();
|
||||
private CommandJigsawPiece piece;
|
||||
private CommandJigsawPool pool;
|
||||
private CommandJigsawConnector connector;
|
||||
private CommandJigsawVariant variant;
|
||||
private CommandJigsawWorkcell workcell;
|
||||
private CommandJigsawRules rules;
|
||||
private CommandJigsawPreview preview;
|
||||
private CommandJigsawAdopt adopt;
|
||||
|
||||
@Director(description = "Create and open a new atomic Jigsaw Studio project", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(name = "key", aliases = {"structure", "name"},
|
||||
description = "New key written as structures/<key>.json") String structure,
|
||||
@Param(description = "planar or spatial", defaultValue = "planar",
|
||||
customHandler = JigsawModeHandler.class) String mode,
|
||||
@Param(description = "iris or vanilla", defaultValue = "iris",
|
||||
customHandler = JigsawCompatibilityHandler.class) String compatibility,
|
||||
@Param(description = "Cell width", defaultValue = "15") int width,
|
||||
@Param(description = "Cell height", defaultValue = "15") int height,
|
||||
@Param(description = "Cell depth", defaultValue = "15") int depth,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
IrisData data = requireData(dimension);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioMode studioMode;
|
||||
JigsawStudioCompatibilityTarget target;
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
studioMode = parseMode(mode);
|
||||
target = parseCompatibility(compatibility);
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
StructureWriteResult result;
|
||||
try {
|
||||
result = JigsawStudioProjectCreator.create(
|
||||
data.getDataFolder().toPath(),
|
||||
new JigsawStudioProjectCreator.Options(structure, studioMode, target, dimensions));
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
Iris.reportError("Failed to create Jigsaw Studio project '" + structure + "'.", exception);
|
||||
sendError("Jigsaw project creation failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!result.successful()) {
|
||||
sendError("Jigsaw project was not created: " + result.status());
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
sendError(result.conflicts().getFirst().relativePath() + ": "
|
||||
+ result.conflicts().getFirst().reason());
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JigsawStudioService.clearAutosaveHistory(
|
||||
data.getDataFolder().toPath(),
|
||||
structure);
|
||||
} catch (IOException exception) {
|
||||
Iris.reportError("Failed to clear stale Jigsaw Studio history for '" + structure + "'.", exception);
|
||||
sendError("Jigsaw project was created, but stale autosave history could not be cleared: "
|
||||
+ exception.getMessage());
|
||||
return;
|
||||
}
|
||||
data.invalidateStructureResources();
|
||||
sender().sendMessage(C.GREEN + "Created Jigsaw project '" + structure + "' atomically.");
|
||||
open(dimension, structure, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Import a registered vanilla or datapack jigsaw graph into Iris Studio",
|
||||
aliases = {"import", "import-vanilla"}, sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void convert(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(description = "Registered structure key such as minecraft:village_plains") String source,
|
||||
@Param(description = "New Iris structure key, or auto", defaultValue = "auto") String target,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
if (studioIsActiveOrOpening()) {
|
||||
sendError("Close the active or opening Jigsaw Studio before importing another graph.");
|
||||
return;
|
||||
}
|
||||
IrisData data = requireData(dimension);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
NamespacedKey sourceKey;
|
||||
String targetKey;
|
||||
try {
|
||||
sourceKey = parseRegisteredStructureKey(source);
|
||||
targetKey = resolveConversionTarget(sourceKey, target);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
VillageImporter.Result result = VillageImporter.importVillage(
|
||||
data,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
StructureImporter.Mode.ADD_ONLY);
|
||||
if (!result.success()) {
|
||||
sendError(conversionFailureMessage(result));
|
||||
return;
|
||||
}
|
||||
data.invalidateStructureResources();
|
||||
VolmitSender commandSender = sender();
|
||||
commandSender.sendMessage(C.GREEN + result.message());
|
||||
CommandJigsawOpen.openProject(player(), commandSender, dimension, targetKey, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Open an existing Iris jigsaw graph in transient Studio",
|
||||
aliases = {"edit", "reopen"}, sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void open(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(name = "key", aliases = {"structure", "name"},
|
||||
description = "Existing key loaded from structures/<key>.json",
|
||||
customHandler = IrisStructureHandler.class) String structure,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
CommandJigsawOpen.openProject(player(), sender(), dimension, structure, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Close the active Jigsaw Studio", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void close(
|
||||
@Param(description = "Discard unsaved Studio layout changes", defaultValue = "false") boolean discard
|
||||
) {
|
||||
Player targetPlayer = player();
|
||||
ActiveContext context = active(targetPlayer);
|
||||
if (context == null) {
|
||||
sendError("You do not have an active Jigsaw Studio session.");
|
||||
return;
|
||||
}
|
||||
if (context.session().isDirty() && !discard) {
|
||||
sendError("This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
UUID requestId = context.request().requestId();
|
||||
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
|
||||
requestId,
|
||||
targetPlayer.getUniqueId(),
|
||||
discard);
|
||||
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
|
||||
sendError(closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
|
||||
? "This Jigsaw Studio is saving. Wait for the save to finish before closing it."
|
||||
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
|
||||
? "This Jigsaw Studio is loading a variant. Wait for it to finish before closing it."
|
||||
: closeStart == JigsawStudioService.CloseStart.DIRTY
|
||||
? "This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true."
|
||||
: closeStart == JigsawStudioService.CloseStart.NOT_OWNER
|
||||
? "This Jigsaw Studio is owned by another player session."
|
||||
: "This Jigsaw Studio session is no longer active.");
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
try {
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null || result == null || result.failureCause() != null) {
|
||||
studioService.cancelClose(requestId);
|
||||
Throwable failure = throwable != null ? throwable : result == null ? null : result.failureCause();
|
||||
commandSender.sendMessage(C.RED + "Jigsaw Studio close failed: "
|
||||
+ (failure == null ? "unknown failure" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
JigsawStudioActivation.deactivate(context.packKey(), requestId);
|
||||
PLAYER_PACKS.remove(targetPlayer.getUniqueId(),
|
||||
new SessionBinding(context.packKey(), requestId));
|
||||
commandSender.sendMessage(C.GREEN + "Jigsaw Studio closed.");
|
||||
}));
|
||||
} catch (Throwable exception) {
|
||||
studioService.cancelClose(requestId);
|
||||
Iris.reportError("Failed to close Jigsaw Studio '" + context.request().structureKey() + "'.", exception);
|
||||
sendError("Jigsaw Studio close failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(name = "delete", aliases = "remove", description = "Delete the active owned jigsaw project",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void delete(
|
||||
@Param(description = "Confirm permanent project deletion", defaultValue = "false") boolean confirm
|
||||
) {
|
||||
if (requireActive() == null) {
|
||||
return;
|
||||
}
|
||||
if (!confirm) {
|
||||
sendError("Project deletion removes every resource owned by this jigsaw. Run again with confirm=true.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().deleteProject(player());
|
||||
}
|
||||
|
||||
@Director(description = "Show active Jigsaw Studio and dynamic evaluation state", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void status() {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioLayout layout = context.session().layout();
|
||||
String selection = context.session().selectedBayId().orElse("none");
|
||||
JigsawStudioVariant activeVariant = "none".equals(selection)
|
||||
? null
|
||||
: context.session().activeVariant(selection).orElse(null);
|
||||
JigsawStudioBay selectedBay = "none".equals(selection) ? null : layout.get(selection);
|
||||
JigsawStudioCellDimensions selectedDimensions = selectedBay == null
|
||||
? null
|
||||
: selectedBay.bounds().dimensions();
|
||||
Optional<JigsawStudioGraphEvaluation> evaluation = JigsawStudioService.get().evaluation(player());
|
||||
sender().sendMessage(C.AQUA + "Jigsaw Studio: " + context.request().structureKey());
|
||||
sender().sendMessage(C.GRAY + "mode=" + layout.mode()
|
||||
+ ", compatibility=" + context.request().compatibilityTarget()
|
||||
+ ", defaultCell=" + layout.cellDimensions().width() + "x"
|
||||
+ layout.cellDimensions().height() + "x" + layout.cellDimensions().depth()
|
||||
+ ", workcells=" + layout.bays().size()
|
||||
+ ", variants=" + layout.variantCatalog().size());
|
||||
sender().sendMessage(C.GRAY + "selected=" + selection
|
||||
+ ", activeVariant=" + (activeVariant == null ? "none" : activeVariant.pieceKey())
|
||||
+ ", size=" + (selectedDimensions == null ? "none" : selectedDimensions.width() + "x"
|
||||
+ selectedDimensions.height() + "x" + selectedDimensions.depth())
|
||||
+ ", enabled=" + (selectedBay == null ? "n/a" : selectedBay.enabled())
|
||||
+ ", autosavePending=" + context.session().isDirty());
|
||||
if (evaluation.isPresent()) {
|
||||
JigsawStudioGraphEvaluation current = evaluation.get();
|
||||
sender().sendMessage(C.GRAY + "evaluation=" + current.state()
|
||||
+ ", seed=" + current.seed()
|
||||
+ ", theme=" + (current.selectedTheme().isBlank() ? "unthemed" : current.selectedTheme())
|
||||
+ ", pieces=" + current.pieceCount()
|
||||
+ ", detail=" + current.detail());
|
||||
} else {
|
||||
sender().sendMessage(C.GRAY + "evaluation=PENDING, seed=" + DEFAULT_STUDIO_SEED);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Open the Jigsaw Studio workcell and variant menu", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void menu() {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().openControlMenu(player());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Select the Jigsaw Studio workcell containing you", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void select() {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
Location location = player().getLocation();
|
||||
JigsawStudioBay bay = context.session().layout().findAt(
|
||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
if (bay == null) {
|
||||
sendError("Stand inside a Jigsaw Studio workcell before selecting it.");
|
||||
return;
|
||||
}
|
||||
context.session().selectBay(bay.stableId());
|
||||
sender().sendMessage(C.GREEN + "Selected " + bay.stableId() + " (" + bay.kind() + ").");
|
||||
}
|
||||
|
||||
@Director(name = "bounds", aliases = {"cell", "resize"},
|
||||
description = "Set the selected Studio workcell capacity",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void bounds(
|
||||
@Param(description = "Cell width") int width,
|
||||
@Param(description = "Cell height") int height,
|
||||
@Param(description = "Cell depth") int depth
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
if (selected == null) {
|
||||
sendError("Select a Jigsaw Studio workcell before changing its capacity.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (dimensions.equals(selected.capacity())) {
|
||||
sender().sendMessage(C.YELLOW + "Workcell capacity is unchanged.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDimensions(player(), selected.stableId(), dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Flush the automatic save for a workcell now", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void save(
|
||||
@Param(description = "Workcell ID or selected", defaultValue = "selected") String bay
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if ("selected".equalsIgnoreCase(bay)) {
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
if (selected == null) {
|
||||
sendError("Select a Jigsaw Studio workcell before flushing its autosave.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().flushAutosave(player(), selected.stableId());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().flushAutosave(player(), bay);
|
||||
}
|
||||
|
||||
@Director(name = "goto", aliases = "teleport", description = "Select and teleport to a Studio workcell",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void gotoBay(
|
||||
@Param(description = "Workcell ID") String bay
|
||||
) {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().teleportTo(player(), bay);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Enable or disable Jigsaw Studio particles", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void particles(
|
||||
@Param(description = "Whether particles are visible") boolean visible
|
||||
) {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().setParticles(player(), visible);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Export the clean active graph as a vanilla datapack", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void export(
|
||||
@Param(description = "Datapack namespace", defaultValue = "iris") String namespace,
|
||||
@Param(description = "Output name under the Studio exports folder", defaultValue = "jigsaw-export")
|
||||
String output,
|
||||
@Param(description = "directory or zip", defaultValue = "zip") String format,
|
||||
@Param(description = "Replace an existing export", defaultValue = "false") boolean replace
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if (context.session().isDirty()) {
|
||||
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
||||
return;
|
||||
}
|
||||
VanillaJigsawExportFormat exportFormat;
|
||||
try {
|
||||
exportFormat = CommandJigsawExports.parseFormat(format);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Path exportRoot = new File(Iris.service(StudioSVC.class).getWorkspaceFolder(), "exports")
|
||||
.toPath().toAbsolutePath().normalize();
|
||||
Path destination;
|
||||
try {
|
||||
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
VanillaJigsawExportRequest request;
|
||||
try {
|
||||
request = VanillaJigsawExportRequest.builder(
|
||||
VanillaJigsawExportSource.forData(
|
||||
context.request().source(), context.request().structureKey()), destination)
|
||||
.namespace(namespace)
|
||||
.resourcePath(context.request().structureKey())
|
||||
.format(exportFormat)
|
||||
.replaceExisting(replace)
|
||||
.build();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
UUID playerId = targetPlayer.getUniqueId();
|
||||
UUID requestId = context.request().requestId();
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
JigsawStudioService.ExportStart exportStart = studioService.tryBeginExport(requestId, playerId);
|
||||
if (exportStart != JigsawStudioService.ExportStart.STARTED) {
|
||||
sendError(CommandJigsawExports.exportStartError(exportStart));
|
||||
return;
|
||||
}
|
||||
if (!CommandJigsawExports.beginExport(playerId, destination)) {
|
||||
studioService.finishExport(requestId);
|
||||
sendError("A vanilla Jigsaw export for this player or output is already in progress.");
|
||||
return;
|
||||
}
|
||||
CommandJigsawExports.ExportLease lease = new CommandJigsawExports.ExportLease(() -> {
|
||||
CommandJigsawExports.finishExport(playerId, destination);
|
||||
studioService.finishExport(requestId);
|
||||
});
|
||||
CommandJigsawExports.ExportOperation operation = new CommandJigsawExports.ExportOperation(
|
||||
targetPlayer,
|
||||
sender(),
|
||||
requestId,
|
||||
context.request().structureKey(),
|
||||
request,
|
||||
lease);
|
||||
sender().sendMessage(C.GRAY + "Exporting '" + operation.structureKey()
|
||||
+ "' in the background to " + destination + "...");
|
||||
try {
|
||||
J.a(() -> CommandJigsawExports.runExport(operation));
|
||||
} catch (RuntimeException exception) {
|
||||
operation.lease().release();
|
||||
Iris.reportError("Failed to schedule vanilla Jigsaw export for '"
|
||||
+ operation.structureKey() + "'.", exception);
|
||||
sendError("Vanilla export could not be scheduled: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static boolean studioIsActiveOrOpening() {
|
||||
return JigsawStudioActivation.activeOwnerId() != null
|
||||
|| JigsawStudioActivation.openingOwnerId() != null;
|
||||
}
|
||||
|
||||
static ActiveContext active(Player player) {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
SessionBinding binding = PLAYER_PACKS.get(player.getUniqueId());
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
ActiveContext context = context(binding);
|
||||
if (context == null) {
|
||||
PLAYER_PACKS.remove(player.getUniqueId(), binding);
|
||||
return null;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private static ActiveContext context(SessionBinding binding) {
|
||||
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
|
||||
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
|
||||
if (request == null || session == null
|
||||
|| !binding.requestId().equals(request.requestId())
|
||||
|| !request.requestId().equals(session.sessionId())) {
|
||||
return null;
|
||||
}
|
||||
return new ActiveContext(binding.packKey(), request, session);
|
||||
}
|
||||
|
||||
static ActiveContext findDirtyContext() {
|
||||
for (SessionBinding binding : PLAYER_PACKS.values()) {
|
||||
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
|
||||
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
|
||||
if (request != null && session != null
|
||||
&& binding.requestId().equals(request.requestId())
|
||||
&& session.isDirty()) {
|
||||
return new ActiveContext(binding.packKey(), request, session);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ActiveContext requireActive() {
|
||||
ActiveContext context = active(player());
|
||||
if (context == null) {
|
||||
sendError("Open a Jigsaw Studio project first.");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private IrisData requireData(IrisDimension dimension) {
|
||||
return requireData(dimension, sender());
|
||||
}
|
||||
|
||||
static IrisData requireData(IrisDimension dimension, VolmitSender commandSender) {
|
||||
IrisData data = dimension == null ? null : dimension.getLoader();
|
||||
if (data == null) {
|
||||
sendError(commandSender, "Could not resolve the requested Iris pack dimension.");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private void sendError(String message) {
|
||||
sender().sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
static void sendError(VolmitSender commandSender, String message) {
|
||||
commandSender.sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
private static JigsawStudioMode parseMode(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "planar", "2d", "planar_jigsaw" -> JigsawStudioMode.PLANAR_JIGSAW;
|
||||
case "spatial", "3d", "spatial_jigsaw" -> JigsawStudioMode.SPATIAL_JIGSAW;
|
||||
default -> throw new IllegalArgumentException("Mode must be planar or spatial.");
|
||||
};
|
||||
}
|
||||
|
||||
private static JigsawStudioCompatibilityTarget parseCompatibility(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "extended", "iris", "iris_extended" -> JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||
case "vanilla", "portable", "vanilla_portable" -> JigsawStudioCompatibilityTarget.VANILLA_PORTABLE;
|
||||
default -> throw new IllegalArgumentException("Compatibility must be iris or vanilla.");
|
||||
};
|
||||
}
|
||||
|
||||
static JigsawStudioCompatibilityTarget compatibilityOf(IrisStructure structure) {
|
||||
return structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE
|
||||
? JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||
: JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||
}
|
||||
|
||||
static NamespacedKey parseRegisteredStructureKey(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Registered structure key").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
NamespacedKey key = NamespacedKey.fromString(normalized);
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Registered structure source must use a valid namespace:path key.");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static String resolveConversionTarget(NamespacedKey source, String value) {
|
||||
Objects.requireNonNull(source, "Registered structure key");
|
||||
String normalized = Objects.requireNonNull(value, "Conversion target").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
if (normalized.isEmpty() || "auto".equals(normalized)) {
|
||||
return StructureImporter.deriveName(source);
|
||||
}
|
||||
StructureKey target = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(target.namespace())) {
|
||||
throw new IllegalArgumentException("Converted Studio targets must use the iris namespace.");
|
||||
}
|
||||
return target.path();
|
||||
}
|
||||
|
||||
static String conversionFailureMessage(VillageImporter.Result result) {
|
||||
String message = Objects.requireNonNull(result, "Village import result").message();
|
||||
String obsoleteInstruction = "; use 'import' for single-template structures";
|
||||
int instructionIndex = message.indexOf(obsoleteInstruction);
|
||||
if (instructionIndex < 0) {
|
||||
return message;
|
||||
}
|
||||
return message.substring(0, instructionIndex)
|
||||
+ ". This Studio converter accepts registered jigsaw structures only.";
|
||||
}
|
||||
|
||||
static JigsawStudioBay selectedBay(ActiveContext context) {
|
||||
String selected = context.session().selectedBayId().orElse(null);
|
||||
return selected == null ? null : context.session().layout().get(selected);
|
||||
}
|
||||
|
||||
static JigsawStudioVariant selectedVariant(ActiveContext context) {
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
return selected == null
|
||||
? null
|
||||
: context.session().activeVariant(selected.stableId()).orElse(null);
|
||||
}
|
||||
|
||||
static boolean runGraphMutation(
|
||||
Player player,
|
||||
ActiveContext context,
|
||||
JigsawStudioService.CommandGraphMutation mutation
|
||||
) {
|
||||
return JigsawStudioService.get().runCommandGraphMutation(
|
||||
player,
|
||||
context.request().requestId(),
|
||||
mutation);
|
||||
}
|
||||
|
||||
static JigsawStudioLayout loadPersistedLayout(ActiveContext context) throws IOException {
|
||||
IrisData source = context.request().source();
|
||||
source.invalidateStructureResources();
|
||||
IrisStructure structure = source.load(IrisStructure.class, context.request().structureKey(), false);
|
||||
if (structure == null) {
|
||||
throw new IOException("The structure could not be reloaded after the graph transaction");
|
||||
}
|
||||
return JigsawStudioGraphMapper.map(source, structure);
|
||||
}
|
||||
|
||||
static JigsawStudioService.CommandGraphMutationResult mappedMutationResult(
|
||||
ActiveContext context,
|
||||
String activateWorkcellId,
|
||||
String activatePieceKey,
|
||||
String message
|
||||
) throws IOException {
|
||||
return new JigsawStudioService.CommandGraphMutationResult(
|
||||
loadPersistedLayout(context),
|
||||
activateWorkcellId,
|
||||
activatePieceKey,
|
||||
message);
|
||||
}
|
||||
|
||||
static String workcellIdForVariant(JigsawStudioLayout layout, String pieceKey) throws IOException {
|
||||
JigsawStudioVariant variant = layout.variantCatalog().find(pieceKey).orElse(null);
|
||||
if (variant == null) {
|
||||
throw new IOException("Persisted variant '" + pieceKey + "' did not map to a Studio workcell");
|
||||
}
|
||||
return variant.archetype()
|
||||
.map(archetype -> archetype.stableId())
|
||||
.orElse(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
|
||||
}
|
||||
|
||||
static PieceWorkcellResolution resolvePieceWorkcell(
|
||||
JigsawStudioLayout layout,
|
||||
IrisJigsawPiece piece,
|
||||
IrisObject object
|
||||
) {
|
||||
JigsawStudioLayout activeLayout = Objects.requireNonNull(layout, "Jigsaw Studio layout");
|
||||
IrisJigsawPiece activePiece = Objects.requireNonNull(piece, "Jigsaw piece");
|
||||
IrisObject activeObject = Objects.requireNonNull(object, "Jigsaw piece object");
|
||||
String workcellId;
|
||||
IrisPosition requiredDimensions;
|
||||
if (activeLayout.mode() == JigsawStudioMode.PLANAR_JIGSAW) {
|
||||
IrisJigsawWorkcellArchetype modelArchetype = IrisJigsawWorkcellArchetype.fromPiece(activePiece);
|
||||
workcellId = JigsawPlanarArchetype.fromModel(modelArchetype).stableId();
|
||||
requiredDimensions = PlanarJigsawWorkcellResolver.canonicalDimensions(activePiece, activeObject);
|
||||
} else {
|
||||
workcellId = JigsawStudioLayout.SPATIAL_WORKCELL_ID;
|
||||
requiredDimensions = new IrisPosition(
|
||||
activeObject.getW(),
|
||||
activeObject.getH(),
|
||||
activeObject.getD());
|
||||
}
|
||||
JigsawStudioBay workcell = activeLayout.get(workcellId);
|
||||
if (workcell == null) {
|
||||
throw new IllegalArgumentException("Workcell '" + workcellId + "' is not configured");
|
||||
}
|
||||
return new PieceWorkcellResolution(workcell, requiredDimensions);
|
||||
}
|
||||
|
||||
static String nextThemeKey(ActiveContext context) throws IOException {
|
||||
IrisStructure structure = context.request().source().load(
|
||||
IrisStructure.class,
|
||||
context.request().structureKey(),
|
||||
false);
|
||||
if (structure == null) {
|
||||
throw new IOException("The active jigsaw structure could not be loaded");
|
||||
}
|
||||
Set<String> declared = new HashSet<>();
|
||||
if (structure.getThemeSets() != null) {
|
||||
for (IrisJigsawThemeSet themeSet : structure.getThemeSets()) {
|
||||
if (themeSet != null && themeSet.getKey() != null) {
|
||||
declared.add(themeSet.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
int candidate = 1;
|
||||
while (candidate > 0) {
|
||||
String key = "variant-" + candidate;
|
||||
if (!declared.contains(key)) {
|
||||
return key;
|
||||
}
|
||||
candidate = Math.incrementExact(candidate);
|
||||
}
|
||||
throw new IOException("Jigsaw Studio cannot allocate another numbered family key");
|
||||
}
|
||||
|
||||
static void sendError(DirectorExecutor executor, String message) {
|
||||
executor.sender().sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
public record ActiveContext(
|
||||
String packKey,
|
||||
JigsawStudioActivation.Request request,
|
||||
JigsawStudioSession session
|
||||
) {
|
||||
}
|
||||
|
||||
record PieceWorkcellResolution(
|
||||
JigsawStudioBay workcell,
|
||||
IrisPosition requiredDimensions
|
||||
) {
|
||||
PieceWorkcellResolution {
|
||||
workcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell");
|
||||
requiredDimensions = Objects.requireNonNull(requiredDimensions, "Jigsaw piece dimensions");
|
||||
}
|
||||
|
||||
boolean fits() {
|
||||
JigsawStudioCellDimensions capacity = workcell.capacity();
|
||||
return requiredDimensions.getX() <= capacity.width()
|
||||
&& requiredDimensions.getY() <= capacity.height()
|
||||
&& requiredDimensions.getZ() <= capacity.depth();
|
||||
}
|
||||
}
|
||||
|
||||
record SessionBinding(String packKey, UUID requestId) {
|
||||
}
|
||||
|
||||
public static final class JigsawModeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("planar", "spatial");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return parseMode(input) == JigsawStudioMode.PLANAR_JIGSAW ? "planar" : "spatial";
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class JigsawCompatibilityHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("iris", "vanilla");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return parseCompatibility(input) == JigsawStudioCompatibilityTarget.IRIS_EXTENDED
|
||||
? "iris" : "vanilla";
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.authoring.StructureKey;
|
||||
import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
|
||||
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionDiagnostic;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionPlan;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionRequest;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionResult;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionService;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionStrategy;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Director(name = "adopt", description = "Inspect and apply safe ownership adoption plans",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawAdopt implements DirectorExecutor {
|
||||
private static final Map<UUID, AdoptionPlanBinding> ADOPTION_PLANS = new ConcurrentHashMap<>();
|
||||
private static final Map<Path, IrisStructureAdoptionService> ADOPTION_SERVICES = new ConcurrentHashMap<>();
|
||||
|
||||
@Director(description = "Inspect an Iris graph for safe Studio ownership adoption", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void inspect(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(description = "Existing Iris structure key") String source,
|
||||
@Param(description = "New owned Iris key, or auto", defaultValue = "auto") String target,
|
||||
@Param(description = "auto, in-place, or clone", defaultValue = "auto",
|
||||
customHandler = JigsawAdoptionStrategyHandler.class) String strategy
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
String sourceKey;
|
||||
Optional<StructureKey> targetKey;
|
||||
IrisStructureAdoptionStrategy adoptionStrategy;
|
||||
Path root;
|
||||
try {
|
||||
sourceKey = parseAdoptionSource(source);
|
||||
targetKey = parseAdoptionTarget(target);
|
||||
adoptionStrategy = parseAdoptionStrategy(strategy);
|
||||
root = canonicalAdoptionRoot(data.getDataFolder().toPath());
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(commandSender, "Could not inspect adoption source: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
UUID ownerId = targetPlayer.getUniqueId();
|
||||
commandSender.sendMessage(C.GRAY + "Inspecting Jigsaw adoption source '" + sourceKey
|
||||
+ "' asynchronously...");
|
||||
try {
|
||||
J.a(() -> runAdoptionInspect(
|
||||
targetPlayer,
|
||||
commandSender,
|
||||
ownerId,
|
||||
dimension,
|
||||
data,
|
||||
root,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
adoptionStrategy));
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to schedule Jigsaw adoption inspection for '"
|
||||
+ sourceKey + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection could not be scheduled: "
|
||||
+ exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Apply one inspected adoption plan without overwriting", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void apply(
|
||||
@Param(description = "Plan UUID returned by adopt inspect",
|
||||
customHandler = JigsawAdoptionPlanHandler.class) String planId
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
UUID parsedPlanId;
|
||||
try {
|
||||
parsedPlanId = UUID.fromString(planId);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan ID must be a UUID.");
|
||||
return;
|
||||
}
|
||||
if (CommandJigsaw.studioIsActiveOrOpening()) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"Close the active or opening Jigsaw Studio before applying an adoption plan.");
|
||||
return;
|
||||
}
|
||||
pruneAdoptionPlanBindings();
|
||||
AdoptionPlanBinding binding = ADOPTION_PLANS.get(parsedPlanId);
|
||||
UUID playerId = player().getUniqueId();
|
||||
if (binding == null || !binding.ownerId().equals(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender, "No active adoption plan '" + parsedPlanId
|
||||
+ "' belongs to your player session. Run adopt inspect again.");
|
||||
return;
|
||||
}
|
||||
if (binding.service().plan(parsedPlanId).isEmpty()) {
|
||||
ADOPTION_PLANS.remove(parsedPlanId, binding);
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId
|
||||
+ "' expired or was already consumed. Run adopt inspect again.");
|
||||
return;
|
||||
}
|
||||
if (!ADOPTION_PLANS.remove(parsedPlanId, binding)) {
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId + "' is already being applied.");
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
commandSender.sendMessage(C.GRAY + "Applying adoption plan '" + parsedPlanId
|
||||
+ "' asynchronously without overwrite...");
|
||||
try {
|
||||
J.a(() -> runAdoptionApply(targetPlayer, commandSender, parsedPlanId, binding));
|
||||
} catch (RuntimeException exception) {
|
||||
ADOPTION_PLANS.putIfAbsent(parsedPlanId, binding);
|
||||
Iris.reportError("Failed to schedule Jigsaw adoption plan '" + parsedPlanId + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption could not be scheduled: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void pruneAdoptionPlanBindings() {
|
||||
for (Map.Entry<UUID, AdoptionPlanBinding> entry : ADOPTION_PLANS.entrySet()) {
|
||||
AdoptionPlanBinding binding = entry.getValue();
|
||||
if (binding.service().plan(entry.getKey()).isEmpty()) {
|
||||
ADOPTION_PLANS.remove(entry.getKey(), binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void runAdoptionApply(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
UUID planId,
|
||||
AdoptionPlanBinding binding
|
||||
) {
|
||||
if (CommandJigsaw.studioIsActiveOrOpening()) {
|
||||
ADOPTION_PLANS.putIfAbsent(planId, binding);
|
||||
J.runEntity(targetPlayer, () -> CommandJigsaw.sendError(commandSender,
|
||||
"Close the active or opening Jigsaw Studio before applying an adoption plan."));
|
||||
return;
|
||||
}
|
||||
IrisStructureAdoptionResult result;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
result = binding.service().apply(binding.plan());
|
||||
} catch (Throwable throwable) {
|
||||
result = null;
|
||||
failure = throwable;
|
||||
Iris.reportError("Failed to apply Jigsaw adoption plan '" + planId + "'.", throwable);
|
||||
}
|
||||
IrisStructureAdoptionResult completedResult = result;
|
||||
Throwable completedFailure = failure;
|
||||
J.runEntity(targetPlayer, () -> finishAdoptionApply(
|
||||
targetPlayer,
|
||||
commandSender,
|
||||
binding,
|
||||
completedResult,
|
||||
completedFailure));
|
||||
}
|
||||
|
||||
private static void runAdoptionInspect(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
Path root,
|
||||
String sourceKey,
|
||||
Optional<StructureKey> targetKey,
|
||||
IrisStructureAdoptionStrategy strategy
|
||||
) {
|
||||
IrisStructureAdoptionService service = null;
|
||||
IrisStructureAdoptionPlan plan = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
IrisStructureAdoptionInputKind inputKind = adoptionInputKind(root, sourceKey);
|
||||
service = ADOPTION_SERVICES.computeIfAbsent(root, IrisStructureAdoptionService::new);
|
||||
plan = service.inspect(new IrisStructureAdoptionRequest(
|
||||
sourceKey,
|
||||
targetKey,
|
||||
strategy,
|
||||
inputKind));
|
||||
} catch (Throwable throwable) {
|
||||
failure = throwable;
|
||||
Iris.reportError("Failed to inspect Jigsaw adoption source '" + sourceKey + "'.", throwable);
|
||||
}
|
||||
IrisStructureAdoptionService completedService = service;
|
||||
IrisStructureAdoptionPlan completedPlan = plan;
|
||||
Throwable completedFailure = failure;
|
||||
J.runEntity(targetPlayer, () -> finishAdoptionInspect(
|
||||
commandSender,
|
||||
ownerId,
|
||||
dimension,
|
||||
data,
|
||||
completedService,
|
||||
completedPlan,
|
||||
completedFailure));
|
||||
}
|
||||
|
||||
private static void finishAdoptionInspect(
|
||||
VolmitSender commandSender,
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
IrisStructureAdoptionService service,
|
||||
IrisStructureAdoptionPlan plan,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || service == null || plan == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
pruneAdoptionPlanBindings();
|
||||
if (service.plan(plan.planId()).isPresent()) {
|
||||
ADOPTION_PLANS.put(plan.planId(), new AdoptionPlanBinding(
|
||||
ownerId, dimension, data, service, plan));
|
||||
}
|
||||
reportAdoptionPlan(commandSender, plan);
|
||||
}
|
||||
|
||||
private static void finishAdoptionApply(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
AdoptionPlanBinding binding,
|
||||
IrisStructureAdoptionResult result,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || result == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
reportAdoptionResult(commandSender, result);
|
||||
if (!result.successful()) {
|
||||
return;
|
||||
}
|
||||
StructureKey target = result.receipt().orElseThrow().targetStructure();
|
||||
binding.data().invalidateStructureResources();
|
||||
CommandJigsawOpen.openProject(targetPlayer, commandSender, binding.dimension(), target.path(), CommandJigsaw.DEFAULT_STUDIO_SEED);
|
||||
}
|
||||
|
||||
private static void reportAdoptionPlan(VolmitSender commandSender, IrisStructureAdoptionPlan plan) {
|
||||
C statusColor = plan.canApply() ? C.GREEN : C.RED;
|
||||
commandSender.sendMessage(statusColor + "Adoption plan " + plan.planId() + " -> "
|
||||
+ plan.disposition() + " for " + plan.targetStructure().value() + ".");
|
||||
commandSender.sendMessage(C.GRAY + "Resources=" + plan.resourceCount()
|
||||
+ ", bytes=" + plan.totalSourceBytes()
|
||||
+ ", errors=" + plan.errorCount()
|
||||
+ ", warnings=" + plan.warningCount() + ".");
|
||||
for (IrisStructureAdoptionDiagnostic diagnostic : plan.diagnostics()) {
|
||||
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
|
||||
}
|
||||
if (plan.canApply()) {
|
||||
commandSender.sendMessage(C.AQUA + "Apply with /iris jigsaw adopt apply " + plan.planId());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reportAdoptionResult(VolmitSender commandSender, IrisStructureAdoptionResult result) {
|
||||
commandSender.sendMessage((result.successful() ? C.GREEN : C.RED)
|
||||
+ "Adoption plan " + result.planId() + ": " + result.status() + ".");
|
||||
for (IrisStructureAdoptionDiagnostic diagnostic : result.diagnostics()) {
|
||||
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
|
||||
}
|
||||
}
|
||||
|
||||
private static C adoptionColor(IrisStructureAdoptionDiagnostic diagnostic) {
|
||||
return switch (diagnostic.severity()) {
|
||||
case ERROR -> C.RED;
|
||||
case WARNING -> C.YELLOW;
|
||||
case INFO -> C.GRAY;
|
||||
};
|
||||
}
|
||||
|
||||
static String parseAdoptionSource(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Adoption source").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
StructureKey source = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(source.namespace())) {
|
||||
throw new IllegalArgumentException("Adoption sources must use the iris namespace.");
|
||||
}
|
||||
return source.path();
|
||||
}
|
||||
|
||||
static Optional<StructureKey> parseAdoptionTarget(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Adoption target").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
if (normalized.isEmpty() || "auto".equals(normalized)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
StructureKey target = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(target.namespace())) {
|
||||
throw new IllegalArgumentException("Adoption targets must use the iris namespace.");
|
||||
}
|
||||
return Optional.of(target);
|
||||
}
|
||||
|
||||
private static IrisStructureAdoptionStrategy parseAdoptionStrategy(String value) {
|
||||
return switch (Objects.requireNonNull(value, "Adoption strategy").trim().toLowerCase(Locale.ROOT)) {
|
||||
case "auto" -> IrisStructureAdoptionStrategy.AUTO;
|
||||
case "in-place", "in_place", "inplace", "claim" -> IrisStructureAdoptionStrategy.IN_PLACE;
|
||||
case "clone", "copy" -> IrisStructureAdoptionStrategy.CLONE;
|
||||
default -> throw new IllegalArgumentException("Adoption strategy must be auto, in-place, or clone.");
|
||||
};
|
||||
}
|
||||
|
||||
static IrisStructureAdoptionInputKind adoptionInputKind(Path packRoot, String source) throws IOException {
|
||||
Path root = canonicalAdoptionRoot(packRoot);
|
||||
StructureKey sourceKey = new StructureKey("iris", parseAdoptionSource(source));
|
||||
Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(sourceKey);
|
||||
if (!Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return IrisStructureAdoptionInputKind.UNOWNED_IRIS;
|
||||
}
|
||||
if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Ownership manifest is not a regular file: " + manifestPath);
|
||||
}
|
||||
StructureOwnershipManifest manifest;
|
||||
try {
|
||||
manifest = StructureOwnershipManifest.fromJson(Files.readAllBytes(manifestPath));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IOException("Invalid ownership manifest at " + manifestPath, exception);
|
||||
}
|
||||
if (!manifest.structure().equals(sourceKey)) {
|
||||
throw new IOException("Ownership manifest at " + manifestPath
|
||||
+ " belongs to " + manifest.structure().value());
|
||||
}
|
||||
return manifest.provenance().origin() == StructureOwnershipManifest.Origin.MANAGED_DATAPACK
|
||||
? IrisStructureAdoptionInputKind.MANAGED_DATAPACK
|
||||
: IrisStructureAdoptionInputKind.UNOWNED_IRIS;
|
||||
}
|
||||
|
||||
private static Path canonicalAdoptionRoot(Path packRoot) throws IOException {
|
||||
Path normalized = Objects.requireNonNull(packRoot, "Adoption pack root").toAbsolutePath().normalize();
|
||||
return Files.exists(normalized, LinkOption.NOFOLLOW_LINKS) ? normalized.toRealPath() : normalized;
|
||||
}
|
||||
|
||||
private record AdoptionPlanBinding(
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
IrisStructureAdoptionService service,
|
||||
IrisStructureAdoptionPlan plan
|
||||
) {
|
||||
}
|
||||
|
||||
public static final class JigsawAdoptionStrategyHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("auto", "in-place", "clone");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return switch (parseAdoptionStrategy(input)) {
|
||||
case AUTO -> "auto";
|
||||
case IN_PLACE -> "in-place";
|
||||
case CLONE -> "clone";
|
||||
};
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class JigsawAdoptionPlanHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return UUID.fromString(input).toString();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException("Adoption plan ID must be a UUID.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.type.Jigsaw;
|
||||
|
||||
@Director(name = "connector", description = "Inspect and configure targeted jigsaw connectors",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawConnector implements DirectorExecutor {
|
||||
@Director(description = "Set the Iris-only channel on the targeted jigsaw marker", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void channel(
|
||||
@Param(description = "Exact channel, or none to clear") String channel
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
Block target = player().getTargetBlockExact(8);
|
||||
if (target == null || !(target.getBlockData() instanceof Jigsaw)) {
|
||||
CommandJigsaw.sendError(this, "Look directly at a jigsaw marker within eight blocks.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay bay = context.session().layout().findAt(
|
||||
target.getX(), target.getY(), target.getZ());
|
||||
JigsawStudioVariant activeVariant = bay == null
|
||||
? null
|
||||
: context.session().activeVariant(bay.stableId()).orElse(null);
|
||||
if (activeVariant == null) {
|
||||
CommandJigsaw.sendError(this, "The targeted marker must be inside a workcell with an active variant.");
|
||||
return;
|
||||
}
|
||||
if (!activeVariant.owned()) {
|
||||
CommandJigsaw.sendError(this, "The active variant is read-only. Adopt it into this project before editing it.");
|
||||
return;
|
||||
}
|
||||
IrisObject object = context.request().source().getObjectLoader().load(activeVariant.objectKey());
|
||||
if (object == null) {
|
||||
CommandJigsaw.sendError(this, "The active variant object '" + activeVariant.objectKey() + "' is missing.");
|
||||
return;
|
||||
}
|
||||
IrisPosition canonicalPosition = new IrisPosition(
|
||||
target.getX() - bay.bounds().originX(),
|
||||
target.getY() - bay.bounds().originY(),
|
||||
target.getZ() - bay.bounds().originZ());
|
||||
IrisPosition position;
|
||||
try {
|
||||
position = activeVariant.canonicalToSourcePosition(
|
||||
canonicalPosition,
|
||||
new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, "Connector channel update failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
String normalized = channel == null || channel.isBlank() || "none".equalsIgnoreCase(channel)
|
||||
? "none"
|
||||
: channel.trim();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.updateConnectorChannel(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
activeVariant.pieceKey(),
|
||||
position,
|
||||
channel);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted connector channel '" + normalized + "' on piece '"
|
||||
+ activeVariant.pieceKey() + "' at " + position.getX() + "," + position.getY()
|
||||
+ "," + position.getZ() + ".");
|
||||
});
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class CommandJigsawExports {
|
||||
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
|
||||
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private CommandJigsawExports() {
|
||||
}
|
||||
|
||||
static String exportStartError(JigsawStudioService.ExportStart exportStart) {
|
||||
Objects.requireNonNull(exportStart, "Jigsaw Studio export start status");
|
||||
return switch (exportStart) {
|
||||
case NOT_ACTIVE -> "The active Jigsaw Studio is no longer available.";
|
||||
case NOT_OWNER -> "Only the Jigsaw Studio owner can export this project.";
|
||||
case DIRTY -> "Wait for the pending autosave or discard the edits before exporting the on-disk graph.";
|
||||
case CLOSING -> "The active Jigsaw Studio is closing and cannot be exported.";
|
||||
case SAVE_IN_PROGRESS -> "Wait for the current Jigsaw Studio save to finish before exporting.";
|
||||
case OPERATION_IN_PROGRESS -> "Wait for the current Jigsaw Studio operation to finish before exporting.";
|
||||
case IN_PROGRESS -> "A Jigsaw Studio export is already in progress.";
|
||||
case STARTED -> throw new IllegalArgumentException("STARTED is not an export failure");
|
||||
};
|
||||
}
|
||||
|
||||
static boolean beginExport(UUID playerId, Path destination) {
|
||||
Objects.requireNonNull(playerId);
|
||||
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
|
||||
if (!EXPORTING_PLAYERS.add(playerId)) {
|
||||
return false;
|
||||
}
|
||||
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
|
||||
return true;
|
||||
}
|
||||
EXPORTING_PLAYERS.remove(playerId);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void finishExport(UUID playerId, Path destination) {
|
||||
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
||||
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
static void runExport(ExportOperation operation) {
|
||||
VanillaJigsawExportResult result = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
result = new VanillaJigsawDatapackExporter().export(operation.request());
|
||||
} catch (Throwable throwable) {
|
||||
failure = throwable;
|
||||
Iris.reportError("Vanilla Jigsaw export failed for '" + operation.structureKey() + "'.", throwable);
|
||||
} finally {
|
||||
operation.lease().release();
|
||||
}
|
||||
VanillaJigsawExportResult completedResult = result;
|
||||
Throwable completedFailure = failure;
|
||||
Runnable response = () -> reportExport(operation, completedResult, completedFailure);
|
||||
J.runEntity(operation.player(), response);
|
||||
}
|
||||
|
||||
private static void reportExport(
|
||||
ExportOperation operation,
|
||||
VanillaJigsawExportResult result,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || result == null) {
|
||||
operation.sender().sendMessage(C.RED + "Background vanilla export for '"
|
||||
+ operation.structureKey() + "' failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
for (VanillaJigsawExportDiagnostic diagnostic : result.diagnostics()) {
|
||||
C color = diagnostic.isBlocking() ? C.RED : C.YELLOW;
|
||||
operation.sender().sendMessage(color.toString() + diagnostic.code() + ": " + diagnostic.message());
|
||||
}
|
||||
CommandJigsaw.ActiveContext current = CommandJigsaw.active(operation.player());
|
||||
boolean sameSession = current != null
|
||||
&& current.request().requestId().equals(operation.requestId());
|
||||
String contextLabel = sameSession ? "Vanilla export" : "Background vanilla export for '"
|
||||
+ operation.structureKey() + "'";
|
||||
operation.sender().sendMessage((result.isSuccess() ? C.GREEN : C.RED) + contextLabel + " "
|
||||
+ result.status() + ": " + result.output());
|
||||
}
|
||||
|
||||
static VanillaJigsawExportFormat parseFormat(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "zip" -> VanillaJigsawExportFormat.ZIP;
|
||||
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
|
||||
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
|
||||
};
|
||||
}
|
||||
|
||||
static Path resolveExportDestination(
|
||||
Path exportRoot,
|
||||
String output,
|
||||
VanillaJigsawExportFormat format
|
||||
) {
|
||||
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
|
||||
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
|
||||
String name = requested.trim();
|
||||
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
|
||||
}
|
||||
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
|
||||
? name + ".zip" : name;
|
||||
Path destination = root.resolve(fileName).normalize();
|
||||
if (destination.equals(root) || !root.equals(destination.getParent())) {
|
||||
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
record ExportOperation(
|
||||
Player player,
|
||||
VolmitSender sender,
|
||||
UUID requestId,
|
||||
String structureKey,
|
||||
VanillaJigsawExportRequest request,
|
||||
ExportLease lease
|
||||
) {
|
||||
}
|
||||
|
||||
static final class ExportLease {
|
||||
private final Runnable release;
|
||||
private final AtomicBoolean released;
|
||||
|
||||
ExportLease(Runnable release) {
|
||||
this.release = Objects.requireNonNull(release, "Jigsaw export lease release action");
|
||||
this.released = new AtomicBoolean();
|
||||
}
|
||||
|
||||
void release() {
|
||||
if (released.compareAndSet(false, true)) {
|
||||
release.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
final class CommandJigsawOpen {
|
||||
private CommandJigsawOpen() {
|
||||
}
|
||||
|
||||
static void openProject(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
IrisDimension dimension,
|
||||
String structure,
|
||||
long seed
|
||||
) {
|
||||
UUID playerId = targetPlayer.getUniqueId();
|
||||
UUID ownerId = JigsawStudioActivation.activeOwnerId();
|
||||
if (ownerId != null && !ownerId.equals(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio is already owned by another player session.");
|
||||
return;
|
||||
}
|
||||
UUID openingOwner = JigsawStudioActivation.openingOwnerId();
|
||||
if (openingOwner != null) {
|
||||
CommandJigsaw.sendError(commandSender, openingOwner.equals(playerId)
|
||||
? "Your Jigsaw Studio is already opening."
|
||||
: "Jigsaw Studio is already opening for another player.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.ActiveContext previous = CommandJigsaw.active(targetPlayer);
|
||||
if (ownerId != null && previous == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Your active Jigsaw Studio binding is unavailable. Close it before opening another project.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.ActiveContext dirty = CommandJigsaw.findDirtyContext();
|
||||
if (dirty != null) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"The current Jigsaw Studio is waiting for autosave. Wait for it to finish or close with discard=true.");
|
||||
return;
|
||||
}
|
||||
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
IrisStructure irisStructure = data.load(IrisStructure.class, structure, false);
|
||||
if (irisStructure == null) {
|
||||
CommandJigsaw.sendError(commandSender, "No Iris jigsaw structure '" + structure + "' exists in this pack.");
|
||||
return;
|
||||
}
|
||||
|
||||
JigsawStudioLayout layout;
|
||||
try {
|
||||
layout = JigsawStudioGraphMapper.map(data, irisStructure);
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to map Jigsaw Studio graph '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Could not map this jigsaw graph: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
String packKey = dimension.getLoadKey();
|
||||
if (!JigsawStudioActivation.tryBeginOpen(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"Jigsaw Studio began opening for another player. Try again after it finishes.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
UUID closingRequestId = previous == null ? null : previous.request().requestId();
|
||||
if (closingRequestId != null) {
|
||||
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
|
||||
closingRequestId,
|
||||
playerId,
|
||||
false);
|
||||
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
CommandJigsaw.sendError(commandSender, closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
|
||||
? "The current Jigsaw Studio is saving. Wait for it to finish before opening another project."
|
||||
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
|
||||
? "The current Jigsaw Studio is loading a variant. Wait for it to finish before opening another project."
|
||||
: closeStart == JigsawStudioService.CloseStart.DIRTY
|
||||
? "The current Jigsaw Studio is waiting for autosave. Wait for it to finish before opening another project."
|
||||
: "The current Jigsaw Studio could not be reserved for replacement.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
JigsawStudioActivation.StagedActivation staged;
|
||||
try {
|
||||
staged = JigsawStudioActivation.stage(
|
||||
packKey,
|
||||
structure,
|
||||
layout.mode(),
|
||||
CommandJigsaw.compatibilityOf(irisStructure),
|
||||
layout.cellDimensions(),
|
||||
data,
|
||||
layout,
|
||||
playerId,
|
||||
closingRequestId);
|
||||
} catch (RuntimeException exception) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
studioService.cancelClose(closingRequestId);
|
||||
Iris.reportError("Failed to stage Jigsaw Studio for '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio staging failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
commandSender.sendMessage(C.GREEN + "Opening transient Jigsaw Studio for '" + structure + "' with "
|
||||
+ layout.bays().size() + " workcells.");
|
||||
|
||||
try {
|
||||
Iris.service(StudioSVC.class).openTracked(
|
||||
commandSender,
|
||||
seed,
|
||||
packKey,
|
||||
CommandJigsaw.STUDIO_OPEN_KIND,
|
||||
() -> beginStagedOpen(staged),
|
||||
world -> finishOpen(targetPlayer, world, staged, studioService))
|
||||
.whenComplete((result, throwable) -> J.s(() -> finishOpenAttempt(
|
||||
commandSender,
|
||||
playerId,
|
||||
structure,
|
||||
closingRequestId,
|
||||
studioService,
|
||||
staged,
|
||||
result,
|
||||
throwable)));
|
||||
} catch (Throwable exception) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
studioService.cancelClose(closingRequestId);
|
||||
JigsawStudioActivation.rollback(staged);
|
||||
Iris.reportError("Failed to open Jigsaw Studio for '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void beginStagedOpen(JigsawStudioActivation.StagedActivation staged) {
|
||||
if (!JigsawStudioActivation.beginStagedGeneration(staged)) {
|
||||
throw new IllegalStateException("The staged Jigsaw Studio activation is no longer current");
|
||||
}
|
||||
}
|
||||
|
||||
private static void finishOpenAttempt(
|
||||
VolmitSender commandSender,
|
||||
UUID playerId,
|
||||
String structureKey,
|
||||
UUID closingRequestId,
|
||||
JigsawStudioService studioService,
|
||||
JigsawStudioActivation.StagedActivation staged,
|
||||
StudioOpenCoordinator.StudioOpenResult result,
|
||||
Throwable throwable
|
||||
) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
if (throwable == null && result != null) {
|
||||
studioService.cancelClose(closingRequestId);
|
||||
return;
|
||||
}
|
||||
studioService.cancelClose(closingRequestId);
|
||||
JigsawStudioActivation.rollback(staged);
|
||||
Throwable failure = unwrapCompletionFailure(throwable == null
|
||||
? new IllegalStateException("Studio open completed without a result")
|
||||
: throwable);
|
||||
if (isExpectedOpenDenial(failure)) {
|
||||
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
|
||||
structureKey, failure.getMessage());
|
||||
return;
|
||||
}
|
||||
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
|
||||
}
|
||||
|
||||
private static void finishOpen(
|
||||
Player targetPlayer,
|
||||
World world,
|
||||
JigsawStudioActivation.StagedActivation staged,
|
||||
JigsawStudioService studioService
|
||||
) {
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Jigsaw Studio opened without a world");
|
||||
}
|
||||
JigsawStudioActivation.Request request = staged.request();
|
||||
JigsawStudioSession session = staged.session();
|
||||
try {
|
||||
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Failed to apply Jigsaw Studio world rules for '" + world.getName() + "'.", exception);
|
||||
}
|
||||
JigsawStudioBay destination = initialBay(session.layout());
|
||||
Location target = destination == null
|
||||
? new Location(world, 0.5D, 66D, 0.5D)
|
||||
: new Location(
|
||||
world,
|
||||
destination.bounds().originX() + destination.bounds().dimensions().width() / 2.0D,
|
||||
destination.bounds().maxY() + 2.0D,
|
||||
destination.bounds().originZ() + destination.bounds().dimensions().depth() / 2.0D);
|
||||
if (destination != null) {
|
||||
session.selectBay(destination.stableId());
|
||||
}
|
||||
if (!JigsawStudioActivation.commit(staged)) {
|
||||
throw new IllegalStateException("The staged Jigsaw Studio activation could not be committed");
|
||||
}
|
||||
studioService.activationCommitted(world, request.requestId());
|
||||
CommandJigsaw.PLAYER_PACKS.put(targetPlayer.getUniqueId(), new CommandJigsaw.SessionBinding(
|
||||
request.packKey(), request.requestId()));
|
||||
try {
|
||||
J.runEntity(targetPlayer, () -> BukkitPlatform.teleportAsync(targetPlayer, target)
|
||||
.thenRun(() -> J.runEntity(targetPlayer, () -> targetPlayer.setGameMode(GameMode.CREATIVE))));
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to move the player to the committed Jigsaw Studio workcell.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static JigsawStudioBay initialBay(JigsawStudioLayout layout) {
|
||||
for (JigsawStudioBay bay : layout.bays()) {
|
||||
if (layout.defaultVariant(bay).isPresent()) {
|
||||
return bay;
|
||||
}
|
||||
}
|
||||
return layout.bays().isEmpty() ? null : layout.bays().getFirst();
|
||||
}
|
||||
|
||||
private static Throwable unwrapCompletionFailure(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current instanceof CompletionException && current.getCause() != null) {
|
||||
current = current.getCause();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static boolean isExpectedOpenDenial(Throwable failure) {
|
||||
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawPlanarTopology;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Director(name = "piece", description = "Manage contextual Studio pieces", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPiece implements DirectorExecutor {
|
||||
@Director(description = "Create an owned piece from the selected Studio topology", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "New owned piece and object key") String pieceKey,
|
||||
@Param(description = "Positive selection weight", defaultValue = "1") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawPlanarTopology topology = null;
|
||||
JigsawStudioBay targetWorkcell = context.session().layout().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
|
||||
if (context.session().layout().mode() == JigsawStudioMode.PLANAR_JIGSAW) {
|
||||
Location location = player().getLocation();
|
||||
JigsawStudioBay contextual = context.session().layout().findAt(
|
||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
if (contextual == null) {
|
||||
contextual = CommandJigsaw.selectedBay(context);
|
||||
} else {
|
||||
context.session().selectBay(contextual.stableId());
|
||||
}
|
||||
if (contextual == null || contextual.topology().isEmpty()) {
|
||||
CommandJigsaw.sendError(this, "Stand in or select a planar topology workcell before creating a variant.");
|
||||
return;
|
||||
}
|
||||
topology = contextual.topology().get();
|
||||
targetWorkcell = contextual;
|
||||
}
|
||||
if (targetWorkcell == null) {
|
||||
CommandJigsaw.sendError(this, "The selected Jigsaw Studio workcell is no longer available.");
|
||||
return;
|
||||
}
|
||||
JigsawPlanarTopology selectedTopology = topology;
|
||||
String activationWorkcellId = targetWorkcell.stableId();
|
||||
JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();
|
||||
String topologyText = selectedTopology == null ? "blank spatial" : selectedTopology.name();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.createPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight,
|
||||
dimensions,
|
||||
selectedTopology);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
activationWorkcellId,
|
||||
pieceKey,
|
||||
"Created owned " + topologyText + " piece '" + pieceKey
|
||||
+ "' and added it to pool '" + poolKey + "' atomically.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Add an existing jigsaw piece to an owned pool transactionally", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void add(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "Existing jigsaw piece key") String pieceKey,
|
||||
@Param(description = "Positive selection weight", defaultValue = "1") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
IrisJigsawPiece piece = context.request().source().getJigsawPieceLoader().load(pieceKey);
|
||||
if (piece == null) {
|
||||
CommandJigsaw.sendError(this, "No jigsaw piece '" + pieceKey + "' exists in this pack.");
|
||||
return;
|
||||
}
|
||||
if (piece.getObject() == null || piece.getObject().isBlank()) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' does not declare an object.");
|
||||
return;
|
||||
}
|
||||
IrisObject object = context.request().source().getObjectLoader().load(piece.getObject());
|
||||
if (object == null) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' has no loadable object.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.PieceWorkcellResolution resolution;
|
||||
try {
|
||||
resolution = CommandJigsaw.resolvePieceWorkcell(context.session().layout(), piece, object);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' cannot be assigned to a Studio workcell: "
|
||||
+ exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!resolution.fits()) {
|
||||
IrisPosition required = resolution.requiredDimensions();
|
||||
JigsawStudioCellDimensions capacity = resolution.workcell().capacity();
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' requires " + required.getX() + "x"
|
||||
+ required.getY() + "x" + required.getZ() + " in " + resolution.workcell().displayName()
|
||||
+ ", but that workcell capacity is " + capacity.width() + "x" + capacity.height()
|
||||
+ "x" + capacity.depth() + ".");
|
||||
return;
|
||||
}
|
||||
String objectKey = piece.getObject();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
if (!JigsawStudioGraphEditor.ownsPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
pieceKey,
|
||||
objectKey)) {
|
||||
throw new IOException("Piece add accepts only piece/object resources already owned by this project; "
|
||||
+ "use piece create for a new owned piece");
|
||||
}
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.addPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("The owned pool is missing or already references '" + pieceKey + "'");
|
||||
}
|
||||
JigsawStudioLayout layout = CommandJigsaw.loadPersistedLayout(context);
|
||||
String activationWorkcellId = CommandJigsaw.workcellIdForVariant(layout, pieceKey);
|
||||
return new JigsawStudioService.CommandGraphMutationResult(
|
||||
layout,
|
||||
activationWorkcellId,
|
||||
pieceKey,
|
||||
"Added '" + pieceKey + "' to pool '" + poolKey + "' in one graph transaction.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Remove the selected piece from an owned pool transactionally", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void remove(
|
||||
@Param(description = "Owned pool resource key") String poolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before removing its pool entry.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.removePiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("The owned pool does not reference selected piece '" + pieceKey + "'");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Removed '" + pieceKey + "' from pool '" + poolKey + "' in one graph transaction.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Allow or forbid cardinal rotation of the selected piece", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void rotatable(
|
||||
@Param(description = "Whether the selected piece may rotate") boolean rotatable
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing rotation.");
|
||||
return;
|
||||
}
|
||||
if (!rotatable
|
||||
&& context.request().compatibilityTarget()
|
||||
== JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
|
||||
CommandJigsaw.sendError(this, "Vanilla-portable pieces must remain rotatable.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.updateRotatable(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
pieceKey,
|
||||
rotatable);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted piece '" + pieceKey + "' rotatable=" + rotatable + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Resize the selected piece object exactly to workcell capacity", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void expand() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay workcell = CommandJigsaw.selectedBay(context);
|
||||
if (workcell == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before expanding its active variant.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().expandVariantToCell(player(), workcell.stableId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "pool", description = "Manage owned jigsaw pools", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPool implements DirectorExecutor {
|
||||
@Director(description = "Create an empty owned jigsaw pool", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "New owned pool resource key") String poolKey,
|
||||
@Param(description = "Owned fallback pool key, or none", defaultValue = "none")
|
||||
String fallbackPoolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.createPool(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
fallback);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Created owned pool '" + poolKey + "' with fallback '"
|
||||
+ (fallback.isBlank() ? "none" : fallback) + "'.");
|
||||
});
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "preview", description = "Assemble a read-only jigsaw preview",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPreview implements DirectorExecutor {
|
||||
@Director(name = "goto", aliases = "teleport", description = "Teleport to the live seed-1337 preview",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void gotoPreview() {
|
||||
JigsawStudioService.get().goToPreview(player());
|
||||
}
|
||||
|
||||
@Director(description = "Assemble a deterministic preview at your location", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void assemble(
|
||||
@Param(description = "Assembly seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
JigsawStudioService.get().previewStudio(player(), seed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioStructureEditor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "rules", aliases = "rule", description = "Manage structure-wide jigsaw rules",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawRules implements DirectorExecutor {
|
||||
@Director(description = "Set maximum expansion depth and horizontal radius", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void limits(
|
||||
@Param(description = "Maximum expansion depth") int maxDepth,
|
||||
@Param(description = "Maximum radius in chunks") int maxSizeChunks
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
if (context.request().compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||
&& (maxDepth > 20 || maxSizeChunks > 8)) {
|
||||
CommandJigsaw.sendError(this, "Vanilla-portable rules require maxDepth <= 20 and maxSizeChunks <= 8.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioStructureEditor.updateLimits(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
maxDepth,
|
||||
maxSizeChunks);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted jigsaw limits: maxDepth=" + maxDepth
|
||||
+ ", maxSizeChunks=" + maxSizeChunks + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Set or clear one owned pool's direct fallback", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void fallback(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "Owned fallback pool key, or none") String fallbackPoolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.updateFallback(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
fallback);
|
||||
if (!update.changed()) {
|
||||
return CommandJigsaw.mappedMutationResult(context, "", "", "Pool fallback is unchanged.");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted pool '" + poolKey + "' fallback as '"
|
||||
+ (fallback.isBlank() ? "none" : fallback) + "'.");
|
||||
});
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Director(name = "variant", description = "Manage selected jigsaw variants", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawVariant implements DirectorExecutor {
|
||||
@Director(description = "Set the selected piece weight in a pool", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void weight(
|
||||
@Param(description = "Pool resource key") String poolKey,
|
||||
@Param(description = "Positive selection weight") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing its weight.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.WeightUpdate update = JigsawStudioPoolEditor.updateWeight(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("Pool '" + poolKey + "' does not reference selected piece '"
|
||||
+ pieceKey + "'");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Updated " + update.changedEntries() + " pool entry weight atomically in "
|
||||
+ update.poolPath() + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Resize only the active variant object within workcell capacity", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void resize(
|
||||
@Param(description = "Variant width") int width,
|
||||
@Param(description = "Variant height") int height,
|
||||
@Param(description = "Variant depth") int depth
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resizing it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().resizeVariant(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Set the active variant's author-facing label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void label(
|
||||
@Param(description = "Display label; quote labels containing spaces") String displayName
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before renaming it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateVariantDisplayName(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
displayName);
|
||||
}
|
||||
|
||||
@Director(name = "label-reset", aliases = "reset-label",
|
||||
description = "Reset the active variant to its resource-key label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void labelReset() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resetting its label.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateVariantDisplayName(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
"");
|
||||
}
|
||||
|
||||
@Director(description = "Duplicate the active variant in only this workcell", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void duplicate() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null || CommandJigsaw.selectedVariant(context) == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before duplicating it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().createVariant(player(), selected.stableId(), true);
|
||||
}
|
||||
|
||||
@Director(name = "duplicate-family", aliases = "family",
|
||||
description = "Duplicate every enabled workcell's active variant as one coherent family",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void duplicateFamily(
|
||||
@Param(description = "Family key or next", defaultValue = "next") String themeKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String selectedTheme = themeKey;
|
||||
if ("next".equalsIgnoreCase(themeKey)) {
|
||||
try {
|
||||
selectedTheme = CommandJigsaw.nextThemeKey(context);
|
||||
} catch (IOException | ArithmeticException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
JigsawStudioService.get().duplicateActiveFamily(player(), selectedTheme);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "workcell", aliases = "cell", description = "Manage the selected Studio workcell",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawWorkcell implements DirectorExecutor {
|
||||
@Director(description = "Set capacity without resizing any variant object", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void capacity(
|
||||
@Param(description = "Capacity width") int width,
|
||||
@Param(description = "Capacity height") int height,
|
||||
@Param(description = "Capacity depth") int depth
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before changing its capacity.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDimensions(
|
||||
player(),
|
||||
selected.stableId(),
|
||||
dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Set the selected workcell's author-facing label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void label(
|
||||
@Param(description = "Display label; quote labels containing spaces") String displayName
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before renaming it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDisplayName(
|
||||
player(),
|
||||
selected.stableId(),
|
||||
displayName);
|
||||
}
|
||||
|
||||
@Director(name = "label-reset", aliases = "reset-label",
|
||||
description = "Reset the selected workcell to its canonical solver label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void labelReset() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before resetting its label.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDisplayName(player(), selected.stableId(), "");
|
||||
}
|
||||
}
|
||||
+74
-58
@@ -16,44 +16,48 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.link.WorldEditLink;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.runtime.ObjectStudioActivation;
|
||||
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.service.ObjectSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.service.WandSVC;
|
||||
import art.arcane.iris.core.tools.IrisConverter;
|
||||
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.integration.WorldEditLink;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.studio.object.ObjectStudioActivation;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.studio.object.ObjectSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.studio.wand.WandSVC;
|
||||
import art.arcane.iris.pack.IrisConverter;
|
||||
import art.arcane.iris.studio.tree.TreePlausibilizeBatch;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.math.Direction;
|
||||
import art.arcane.iris.command.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ObjectScaleHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ObjectTargetHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.math.DirectionBasis;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
@@ -82,11 +86,11 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation", descriptionKey = "iris.director.commandobject.director.iris_object_manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
static final Set<Material> PASTE_TRANSPARENT_BLOCKS = Set.of(
|
||||
@@ -221,11 +225,19 @@ public class CommandObject implements DirectorExecutor {
|
||||
//Prevent blocks being set in or bellow bedrock
|
||||
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
|
||||
|
||||
futureBlockChanges.putIfAbsent(block, block.getBlockData());
|
||||
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
futureBlockChanges.computeIfAbsent(block, target -> {
|
||||
BlockData previous = target.getBlockData();
|
||||
return external == null ? previous : external.captureBlockData(previous);
|
||||
});
|
||||
|
||||
if (d instanceof IrisCustomData data) {
|
||||
if (external != null && external.placeBlock(block, data.getCustom())) {
|
||||
return;
|
||||
}
|
||||
block.setBlockData(data.getBase(), false);
|
||||
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
|
||||
IrisLogging.warnOnce("object-paste:external:" + data.getCustom(),
|
||||
"Direct placement of %s is unavailable; only its base block was placed.", data.getCustom());
|
||||
} else block.setBlockData(d, false);
|
||||
}
|
||||
|
||||
@@ -471,9 +483,9 @@ public class CommandObject implements DirectorExecutor {
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
|
||||
DirectionBasis d = DirectionBasis.closest(player.getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d.f(), -amount);
|
||||
cursor = cursor.expand(d.getCuboidDirection(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
@@ -527,21 +539,19 @@ public class CommandObject implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingIrisWand(player)) {
|
||||
Location[] g = WandSVC.getCuboid(player);
|
||||
Location[] g = WandSVC.getCuboid(player);
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -553,17 +563,21 @@ public class CommandObject implements DirectorExecutor {
|
||||
boolean edit,
|
||||
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
|
||||
int rotate,
|
||||
@Param(description = "The factor by which to scale the object placement", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "1")
|
||||
double scale
|
||||
@Param(description = "Explicit scale factor, or dimension to inherit the current Iris world's factor", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "dimension", customHandler = ObjectScaleHandler.class)
|
||||
Double scale
|
||||
// ,
|
||||
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
|
||||
// IrisObjectPlacementScaleInterpolator interpolator
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
Engine placementEngine = engine();
|
||||
double factor = scale == null
|
||||
? placementEngine == null ? 1D : placementEngine.getDimension().getAllObjectScaleFactor()
|
||||
: IrisObjectScale.requireValidFactor(scale, "scale");
|
||||
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
||||
if (scale > maxScale) {
|
||||
if (factor > maxScale) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale)));
|
||||
scale = maxScale;
|
||||
factor = maxScale;
|
||||
}
|
||||
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
@@ -583,14 +597,16 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
Map<Block, BlockData> futureChanges = new HashMap<>();
|
||||
|
||||
if (scale != 1) {
|
||||
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
|
||||
if (factor != 1D) {
|
||||
o = scale == null ? IrisObjectScale.getFixed(o, factor)
|
||||
: new IrisObjectScale().setSize(factor)
|
||||
.setInterpolation(IrisObjectPlacementScaleInterpolator.TRICUBIC).get(new RNG(), o);
|
||||
}
|
||||
|
||||
// Block writes must run on the thread owning the target chunk; the undo log stays global.
|
||||
final IrisObject placed = o;
|
||||
if (!J.runAt(block, () -> {
|
||||
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), null);
|
||||
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), placed.getLoader());
|
||||
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
|
||||
|
||||
if (!edit) {
|
||||
@@ -693,7 +709,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
|
||||
DirectionBasis d = DirectionBasis.closest(player.getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
+100
-12
@@ -16,30 +16,36 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.core.pack.PackValidationCache;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.localization.TextKey;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
/**
|
||||
@@ -90,6 +96,18 @@ public class CommandPack implements DirectorExecutor {
|
||||
persistValidationCache(packsRoot);
|
||||
}
|
||||
|
||||
@Director(description = "Package a validated dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
|
||||
@@ -235,6 +253,76 @@ public class CommandPack implements DirectorExecutor {
|
||||
reportResult(s, result);
|
||||
}
|
||||
|
||||
@Director(description = "Show pack content that cannot generate on this Minecraft version", descriptionKey = "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version", aliases = {"cp"})
|
||||
public void compat(
|
||||
@Param(description = "The pack folder name to report on, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_compat", defaultValue = "*")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (wantsAllPacks(pack)) {
|
||||
Map<String, PackValidationResult> snapshot = PackValidationRegistry.snapshot();
|
||||
if (snapshot.isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, PackValidationResult> entry : snapshot.entrySet()) {
|
||||
for (String line : compatOutput(entry.getKey(), entry.getValue())) {
|
||||
s.sendMessage(line);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (String line : compatOutput(pack, PackValidationRegistry.get(pack))) {
|
||||
s.sendMessage(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The compat listing for one pack, straight from the persisted validation so no pack is reloaded. Every key is
|
||||
* printed with every subject; the boot listing is the capped form of the same data.
|
||||
*/
|
||||
static List<String> compatOutput(String pack, PackValidationResult result) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (result == null) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE,
|
||||
MessageArgument.untrusted("pack", String.valueOf(pack)),
|
||||
MessageArgument.untrusted("pack2", String.valueOf(pack))));
|
||||
return lines;
|
||||
}
|
||||
String version = compatVersion(result);
|
||||
if (result.getCompatFindings().isEmpty()) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_NONE,
|
||||
MessageArgument.untrusted("pack", pack),
|
||||
MessageArgument.untrusted("version", version)));
|
||||
return lines;
|
||||
}
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_HEADER,
|
||||
MessageArgument.untrusted("pack", pack),
|
||||
MessageArgument.untrusted("version", version)));
|
||||
for (String line : PackCompatReport.of(result.getCompatFindings()).keyLines(0)) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2,
|
||||
MessageArgument.untrusted("value", line)));
|
||||
}
|
||||
lines.add(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_COMPAT_REMEDY));
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The live Minecraft version, falling back to the one the validation ran against. */
|
||||
static String compatVersion(PackValidationResult result) {
|
||||
if (IrisPlatforms.isBound()) {
|
||||
String live = IrisPlatforms.get().minecraftVersion();
|
||||
if (live != null && !live.isBlank()) {
|
||||
return live;
|
||||
}
|
||||
}
|
||||
String persisted = result.getMinecraftVersion();
|
||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||
}
|
||||
|
||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packFolder);
|
||||
+10
-11
@@ -16,25 +16,24 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.world.pregen.PregenTask;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.ModdedCommandMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@@ -62,7 +61,7 @@ public class CommandPregen implements DirectorExecutor {
|
||||
|
||||
if (PregeneratorJob.getInstance() != null) {
|
||||
// Reject like the modded adapter does; never silently kill a running job.
|
||||
sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
|
||||
sender().sendMessage(IrisLanguage.text(IrisMessages.PREGEN_ALREADY_RUNNING));
|
||||
return;
|
||||
}
|
||||
|
||||
+55
-29
@@ -16,19 +16,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.commands.CommandIris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.iris.util.common.director.DirectorContextHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorSystem;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.cache.AtomicCache;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.DirectorEngineOptions;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
|
||||
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
|
||||
@@ -55,18 +51,23 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.IrisMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
private static final String ROOT_PERMISSION = "iris.all";
|
||||
|
||||
private final ThreadLocal<Deque<LanguageAudience.Scope>> languageScopes = ThreadLocal.withInitial(ArrayDeque::new);
|
||||
|
||||
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
|
||||
private final transient ThreadLocal<CommandSender> dispatchSenders = new ThreadLocal<>();
|
||||
|
||||
@@ -102,6 +103,12 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
|
||||
private DirectorContextRegistry buildDirectorContexts() {
|
||||
DirectorContextRegistry contexts = new DirectorContextRegistry();
|
||||
contexts.register(CommandSender.class, (invocation, map) -> {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
return sender.sender();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
|
||||
registerContextHandler(contexts, entry.getKey(), entry.getValue());
|
||||
@@ -139,6 +146,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
@Override
|
||||
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
languageScopes.get().push(LanguageAudience.open(sender.sender() instanceof Player player ? player.getUniqueId() : null));
|
||||
DirectorContext.touch(new VolmitSender(sender.sender()));
|
||||
}
|
||||
}
|
||||
@@ -146,6 +154,13 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
@Override
|
||||
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
DirectorContext.remove();
|
||||
Deque<LanguageAudience.Scope> scopes = languageScopes.get();
|
||||
if (!scopes.isEmpty()) {
|
||||
scopes.pop().close();
|
||||
}
|
||||
if (scopes.isEmpty()) {
|
||||
languageScopes.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -169,7 +184,12 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
}
|
||||
|
||||
public void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
if (!sender.hasPermission(ROOT_PERMISSION)) {
|
||||
if (args.length > 0 && args[0].equalsIgnoreCase("language")) {
|
||||
Iris.instance.selectLanguage(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
return;
|
||||
}
|
||||
if (!(args.length > 1 && args[0].equalsIgnoreCase("debug") && args[1].equalsIgnoreCase("dump"))
|
||||
&& !sender.hasPermission(ROOT_PERMISSION)) {
|
||||
ComponentMessenger.sendSection(sender, IrisLanguage.text(
|
||||
IrisMessages.COMMAND_PERMISSION_DENIED,
|
||||
MessageArgument.trusted("permission", ROOT_PERMISSION)
|
||||
@@ -181,7 +201,9 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
}
|
||||
|
||||
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
List<String> suggestions = runDirectorTab(sender, alias, args);
|
||||
List<String> suggestions = args.length > 0 && args[0].equalsIgnoreCase("language")
|
||||
? Iris.instance.completeLanguage(sender, Arrays.copyOfRange(args, 1, args.length))
|
||||
: runDirectorTab(sender, alias, args);
|
||||
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
|
||||
}
|
||||
@@ -199,7 +221,7 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
private void registerPaperCommand() {
|
||||
try {
|
||||
Class<?> registrarType = Class.forName(
|
||||
"art.arcane.iris.core.commands.PaperCommandRegistrar",
|
||||
"art.arcane.iris.command.PaperCommandRegistrar",
|
||||
true,
|
||||
getClass().getClassLoader()
|
||||
);
|
||||
@@ -235,35 +257,39 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
}
|
||||
|
||||
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
|
||||
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
|
||||
if (request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
|
||||
if (request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DirectorMiniMenu.deliver(
|
||||
sender,
|
||||
request.get(),
|
||||
DirectorMiniMenu.Theme.irisGreen(),
|
||||
IrisLanguage.directorResolver()
|
||||
);
|
||||
return true;
|
||||
DirectorMiniMenu.deliver(
|
||||
sender,
|
||||
request.get(),
|
||||
DirectorMiniMenu.Theme.irisGreen(),
|
||||
IrisLanguage.directorResolver()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||
dispatchSenders.set(sender);
|
||||
try {
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return DirectorExecutionResult.notHandled();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
dispatchSenders.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try {
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
+29
-30
@@ -16,40 +16,39 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureCaptureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureIndexService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.structure.StructureCaptureImporter;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.structure.StructureIndexService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.PlacedStructurePiece;
|
||||
import art.arcane.iris.structure.placement.StructureAssembler;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.structure.graph.StructureAssemblyResult;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -67,10 +66,10 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)", descriptionKey = "iris.director.commandstructure.director.iris_structure_tools_index_import_info")
|
||||
public class CommandStructure implements DirectorExecutor {
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", descriptionKey = "iris.director.commandstructure.director.regenerate_structure_index_json_listing_all_vanilla_datapack_iris_structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
|
||||
+48
-60
@@ -16,63 +16,63 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.ImageMapStudioGUI;
|
||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.core.service.BoardSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.FeatureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
||||
import art.arcane.iris.studio.view.VisionGUI;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.structure.FeatureImporter;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -101,10 +101,10 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
|
||||
@@ -275,10 +275,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
if (noGUI()) {
|
||||
return;
|
||||
}
|
||||
if (!ImageMapStudioGUI.isAvailable()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
|
||||
return;
|
||||
}
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||
return;
|
||||
@@ -420,18 +416,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
@Director(description = "Package a dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
@@ -874,7 +858,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
|
||||
art.arcane.iris.generation.geometry.IrisBlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
@@ -896,6 +880,10 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
|
||||
return true;
|
||||
}
|
||||
if (!GuiHost.isAvailable()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DESKTOP_UNAVAILABLE));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+45
-23
@@ -16,29 +16,29 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.edit.BlockSignal;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.studio.edit.BlockSignal;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Chunk;
|
||||
@@ -53,9 +53,9 @@ import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?", descriptionKey = "iris.director.commandwhat.director.iris_what")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@@ -82,21 +82,36 @@ public class CommandWhat implements DirectorExecutor {
|
||||
|
||||
@Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER)
|
||||
public void biome() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
onPlayerThread(player, () -> describeBiome(commandSender, player));
|
||||
}
|
||||
|
||||
private void describeBiome(VolmitSender commandSender, Player player) {
|
||||
Location location = player.getLocation();
|
||||
try {
|
||||
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
|
||||
Engine engine = IrisToolbelt.access(location.getWorld()).getEngine();
|
||||
IrisBiome b = engine.getBiome(location.getBlockX(), location.getBlockY() - location.getWorld().getMinHeight(), location.getBlockZ());
|
||||
Biome derivative = b.getDerivative();
|
||||
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
|
||||
|
||||
} catch (SavedBiomeUnavailableException e) {
|
||||
if (e.getCause() != null) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
commandSender.sendMessage(e.isLoading()
|
||||
? "Saved biome data is loading. Run the command again shortly."
|
||||
: e.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Biome biome = player().getLocation().getBlock().getBiome();
|
||||
Biome biome = location.getBlock().getBiome();
|
||||
NamespacedKey key = resolveBiomeKey(biome);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
|
||||
|
||||
if (key == null || key.getKey().equals("custom")) {
|
||||
try {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(player().getLocation())), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())))));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(location)), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(location)))));
|
||||
} catch (Throwable ee) {
|
||||
Iris.reportError(ee);
|
||||
}
|
||||
@@ -118,6 +133,13 @@ public class CommandWhat implements DirectorExecutor {
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
|
||||
|
||||
} catch (SavedBiomeUnavailableException e) {
|
||||
if (e.getCause() != null) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
commandSender.sendMessage(e.isLoading()
|
||||
? "Saved biome data is loading. Run the command again shortly."
|
||||
: e.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.CommandSVC;
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
@@ -11,7 +10,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Paper-only command registration. Lives outside {@code art.arcane.iris.core.service} on purpose:
|
||||
* Paper-only command registration. Lives outside {@code art.arcane.iris.generation.runtime} on purpose:
|
||||
* that package is eagerly Class.forName-scanned by JarScanner during enable, and resolving this
|
||||
* class on plain Spigot throws NoClassDefFoundError for the Paper lifecycle types. CommandSVC
|
||||
* loads it reflectively only when the Bukkit plugin.yml command is unavailable (Paper path).
|
||||
-2209
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import art.arcane.volmlib.util.bukkit.papi.PlaceholderValues;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record IrisPapiWorldView(
|
||||
IrisPapiPosition position,
|
||||
long builtAtMs,
|
||||
String available,
|
||||
String biome,
|
||||
String biomeKey,
|
||||
String region,
|
||||
String regionKey,
|
||||
String dimension) {
|
||||
public static IrisPapiWorldView absent(IrisPapiPosition position, long builtAtMs) {
|
||||
return new IrisPapiWorldView(
|
||||
position,
|
||||
builtAtMs,
|
||||
PlaceholderValues.FALSE,
|
||||
PlaceholderValues.UNAVAILABLE,
|
||||
PlaceholderValues.UNAVAILABLE,
|
||||
PlaceholderValues.UNAVAILABLE,
|
||||
PlaceholderValues.UNAVAILABLE,
|
||||
PlaceholderValues.UNAVAILABLE);
|
||||
}
|
||||
|
||||
public static IrisPapiWorldView present(
|
||||
IrisPapiPosition position,
|
||||
long builtAtMs,
|
||||
Optional<String> biome,
|
||||
Optional<String> biomeKey,
|
||||
Optional<String> region,
|
||||
Optional<String> regionKey,
|
||||
Optional<String> dimension) {
|
||||
return new IrisPapiWorldView(
|
||||
position,
|
||||
builtAtMs,
|
||||
PlaceholderValues.TRUE,
|
||||
text(biome),
|
||||
text(biomeKey),
|
||||
text(region),
|
||||
text(regionKey),
|
||||
text(dimension));
|
||||
}
|
||||
|
||||
static String text(Optional<String> value) {
|
||||
return value == null || value.isEmpty() ? PlaceholderValues.UNAVAILABLE : PlaceholderValues.text(value.get());
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package art.arcane.iris.core.service.terrain;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisSurfaceKind;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
|
||||
public final class IrisSurfaceClassifier {
|
||||
private IrisSurfaceClassifier() {
|
||||
}
|
||||
|
||||
public static boolean requiresSurfaceBiome(int engineSurfaceHeight, int engineFluidHeight) {
|
||||
return engineSurfaceHeight > 0 && engineSurfaceHeight > engineFluidHeight;
|
||||
}
|
||||
|
||||
public static IrisSurfaceKind classify(int engineSurfaceHeight, int engineFluidHeight, InferredType inferredType) {
|
||||
if (engineSurfaceHeight <= 0) {
|
||||
return IrisSurfaceKind.VOID;
|
||||
}
|
||||
|
||||
if (engineSurfaceHeight <= engineFluidHeight) {
|
||||
return IrisSurfaceKind.OCEAN;
|
||||
}
|
||||
|
||||
return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND;
|
||||
}
|
||||
}
|
||||
+16
-19
@@ -1,24 +1,21 @@
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.lifecycle.WorldUnloadBoundaryRegistry;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.world.lifecycle.WorldUnloadBoundaryRegistry;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream3D;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -153,7 +150,7 @@ public final class IrisEngineSVC implements IrisService {
|
||||
metrics.get()));
|
||||
}
|
||||
|
||||
IrisTelemetrySnapshot telemetrySnapshot() {
|
||||
public IrisTelemetrySnapshot telemetrySnapshot() {
|
||||
return telemetry.get();
|
||||
}
|
||||
|
||||
+9
-10
@@ -1,19 +1,18 @@
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream3D;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
final class IrisEngineStatus {
|
||||
private IrisEngineStatus() {
|
||||
+14
-15
@@ -1,10 +1,9 @@
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record IrisTelemetrySnapshot(
|
||||
public record IrisTelemetrySnapshot(
|
||||
long sampledAtMs,
|
||||
List<EngineTelemetrySnapshot> worlds,
|
||||
EngineTelemetrySnapshot.Aggregate aggregate,
|
||||
@@ -17,7 +16,7 @@ record IrisTelemetrySnapshot(
|
||||
CacheSnapshot caches,
|
||||
PregenSnapshot pregenerator
|
||||
) {
|
||||
static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
|
||||
public static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
|
||||
0L,
|
||||
List.of(),
|
||||
EngineTelemetrySnapshot.aggregate(List.of()),
|
||||
@@ -31,7 +30,7 @@ record IrisTelemetrySnapshot(
|
||||
PregenSnapshot.INACTIVE
|
||||
);
|
||||
|
||||
IrisTelemetrySnapshot {
|
||||
public IrisTelemetrySnapshot {
|
||||
worlds = worlds == null ? List.of() : List.copyOf(worlds);
|
||||
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
|
||||
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
|
||||
@@ -48,14 +47,14 @@ record IrisTelemetrySnapshot(
|
||||
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
|
||||
}
|
||||
|
||||
record CacheSnapshot(
|
||||
public record CacheSnapshot(
|
||||
CacheBucket total,
|
||||
CacheBucket resource,
|
||||
CacheBucket stream2d,
|
||||
CacheBucket stream3d,
|
||||
CacheBucket other
|
||||
) {
|
||||
static final CacheSnapshot EMPTY = new CacheSnapshot(
|
||||
public static final CacheSnapshot EMPTY = new CacheSnapshot(
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY,
|
||||
@@ -63,7 +62,7 @@ record IrisTelemetrySnapshot(
|
||||
CacheBucket.EMPTY
|
||||
);
|
||||
|
||||
CacheSnapshot {
|
||||
public CacheSnapshot {
|
||||
total = total == null ? CacheBucket.EMPTY : total;
|
||||
resource = resource == null ? CacheBucket.EMPTY : resource;
|
||||
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
|
||||
@@ -72,16 +71,16 @@ record IrisTelemetrySnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
record CacheBucket(int count, long entries, long capacity) {
|
||||
static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
|
||||
public record CacheBucket(int count, long entries, long capacity) {
|
||||
public static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
|
||||
|
||||
CacheBucket {
|
||||
public CacheBucket {
|
||||
count = Math.max(0, count);
|
||||
entries = Math.max(0L, entries);
|
||||
capacity = Math.max(0L, capacity);
|
||||
}
|
||||
|
||||
double usage() {
|
||||
public double usage() {
|
||||
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
|
||||
}
|
||||
|
||||
@@ -93,7 +92,7 @@ record IrisTelemetrySnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
record PregenSnapshot(
|
||||
public record PregenSnapshot(
|
||||
boolean active,
|
||||
String worldIdentity,
|
||||
String worldName,
|
||||
@@ -107,7 +106,7 @@ record IrisTelemetrySnapshot(
|
||||
long elapsedMs,
|
||||
long failed
|
||||
) {
|
||||
static final PregenSnapshot INACTIVE = new PregenSnapshot(
|
||||
public static final PregenSnapshot INACTIVE = new PregenSnapshot(
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
@@ -122,7 +121,7 @@ record IrisTelemetrySnapshot(
|
||||
0L
|
||||
);
|
||||
|
||||
PregenSnapshot {
|
||||
public PregenSnapshot {
|
||||
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
|
||||
worldName = worldName == null ? "" : worldName.trim();
|
||||
progressPercent = Double.isFinite(progressPercent)
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.api.IrisApiEventSVC;
|
||||
|
||||
import art.arcane.iris.api.world.IrisWorldPhase;
|
||||
import org.bukkit.World;
|
||||
+6
-3
@@ -1,9 +1,12 @@
|
||||
package art.arcane.iris.core.service;
|
||||
package art.arcane.iris.integration;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngineSVC;
|
||||
import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.EngineTelemetrySnapshot;
|
||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||
+14
-1
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.link;
|
||||
package art.arcane.iris.integration;
|
||||
|
||||
import art.arcane.volmlib.util.bukkit.papi.PlaceholderKeyRegistry;
|
||||
import art.arcane.volmlib.util.bukkit.papi.VolmitPlaceholderExpansion;
|
||||
@@ -42,6 +42,19 @@ public final class IrisPapiExpansion extends VolmitPlaceholderExpansion {
|
||||
.key("world.available", state::worldAvailable)
|
||||
.key("world.biome", state::biome)
|
||||
.key("world.biome-key", state::biomeKey)
|
||||
.key("world.biome-custom", state::biomeCustom)
|
||||
.key("world.biome-custom-id", state::biomeCustomId)
|
||||
.key("world.biome-custom-ids", state::biomeCustomIds)
|
||||
.key("world.biome-custom-key", state::biomeCustomKey)
|
||||
.key("world.biome-custom-keys", state::biomeCustomKeys)
|
||||
.key("world.biome-custom-count", state::biomeCustomCount)
|
||||
.key("world.biome-derivative", state::biomeDerivative)
|
||||
.key("world.biome-vanilla-derivative", state::biomeVanillaDerivative)
|
||||
.key("world.biome-type", state::biomeType)
|
||||
.key("world.min-height", state::minHeight)
|
||||
.key("world.max-height", state::maxHeight)
|
||||
.key("world.height", state::height)
|
||||
.key("world.fluid-height", state::fluidHeight)
|
||||
.key("world.region", state::region)
|
||||
.key("world.region-key", state::regionKey)
|
||||
.key("world.dimension", state::dimension)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user