mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
dd
This commit is contained in:
@@ -26,65 +26,6 @@ jobs:
|
|||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
- name: Run verification gates
|
- name: Run verification gates
|
||||||
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
|
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
|
- name: Run modded shared tests
|
||||||
build-artifacts:
|
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||||
name: Build ${{ matrix.platform }}
|
|
||||||
needs: verify
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: bukkit
|
|
||||||
task: buildBukkit
|
|
||||||
- platform: fabric
|
|
||||||
task: buildFabric
|
|
||||||
- platform: forge
|
|
||||||
task: buildForge
|
|
||||||
- platform: neoforge
|
|
||||||
task: buildNeoforge
|
|
||||||
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: Build ${{ matrix.platform }} artifact
|
|
||||||
run: ./gradlew ${{ matrix.task }} -PuseLocalVolmLib=false --console=plain --stacktrace
|
|
||||||
- name: Upload ${{ matrix.platform }} jar
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: iris-${{ matrix.platform }}
|
|
||||||
path: dist/*.jar
|
|
||||||
retention-days: 14
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
release-bundle:
|
|
||||||
name: Release bundle (buildAll)
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
needs: [verify, build-artifacts]
|
|
||||||
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: Build all platforms
|
|
||||||
run: ./gradlew buildAll --no-parallel -PuseLocalVolmLib=false --console=plain --stacktrace
|
|
||||||
- name: Upload release bundle
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: iris-release-bundle
|
|
||||||
path: dist/*.jar
|
|
||||||
retention-days: 90
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|||||||
+14
@@ -12,6 +12,7 @@ collection/
|
|||||||
|
|
||||||
out/
|
out/
|
||||||
tools/simd-bench/out/
|
tools/simd-bench/out/
|
||||||
|
tools/simd-bench/simd-bench.jar
|
||||||
|
|
||||||
DataPackExamples/
|
DataPackExamples/
|
||||||
|
|
||||||
@@ -25,4 +26,17 @@ adapters/*/run/
|
|||||||
|
|
||||||
plans/
|
plans/
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
local.properties
|
||||||
|
*.jks
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.hprof
|
||||||
|
credentials.json
|
||||||
|
service-account*.json
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
|
|||||||
@@ -73,9 +73,16 @@ Mod (positional arguments):
|
|||||||
/iris create myworld overworld 1337
|
/iris create myworld overworld 1337
|
||||||
```
|
```
|
||||||
|
|
||||||
`/iris pregen start` pregenerates around spawn; players with the client mod see the native HUD,
|
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
|
||||||
everyone else gets a boss bar (modded) or console/status output. `/iris pregen status` reports
|
servers they are positional and composable:
|
||||||
progress on the plugin.
|
|
||||||
|
```
|
||||||
|
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||||
|
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
|
||||||
|
console/status output. `/iris pregen status` reports progress on the plugin.
|
||||||
|
|
||||||
## Studio and VSCode workspace
|
## Studio and VSCode workspace
|
||||||
|
|
||||||
@@ -104,7 +111,7 @@ files hotloads the changes and regenerates the schemas.
|
|||||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||||
|
|
||||||
```
|
```
|
||||||
./gradlew buildAll
|
./gradlew buildAllToOut
|
||||||
```
|
```
|
||||||
|
|
||||||
builds every platform artifact into `dist/`:
|
builds every platform artifact into `dist/`:
|
||||||
|
|||||||
+40
-3
@@ -25,6 +25,9 @@ import org.bukkit.craftbukkit.CraftWorld;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -40,6 +43,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private final Registry<Biome> biomeRegistry;
|
private final Registry<Biome> biomeRegistry;
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||||
private final KMap<String, Holder<Biome>> customBiomes;
|
private final KMap<String, Holder<Biome>> customBiomes;
|
||||||
|
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||||
private final Holder<Biome> fallbackBiome;
|
private final Holder<Biome> fallbackBiome;
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||||
@@ -51,6 +55,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).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.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||||
|
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
||||||
@@ -163,6 +168,38 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return m;
|
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;
|
||||||
|
}
|
||||||
|
return vanillaSpawnBiomes.get(biome.value());
|
||||||
|
}
|
||||||
|
|
||||||
private RegistryAccess registry() {
|
private RegistryAccess registry() {
|
||||||
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||||
}
|
}
|
||||||
@@ -242,8 +279,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||||
? resolution.irisBiome.getGroundBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ)
|
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||||
: resolution.irisBiome.getSkyBiome(resolution.rng, 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);
|
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
return holder;
|
return holder;
|
||||||
@@ -253,7 +290,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ);
|
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||||
if (customBiome != null) {
|
if (customBiome != null) {
|
||||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
|
|||||||
+48
-1
@@ -25,6 +25,7 @@ import net.minecraft.world.level.levelgen.WorldgenRandom;
|
|||||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -40,7 +41,9 @@ import net.minecraft.resources.Identifier;
|
|||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.WorldGenRegion;
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.util.random.Weighted;
|
||||||
import net.minecraft.util.random.WeightedList;
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
import net.minecraft.world.entity.MobCategory;
|
import net.minecraft.world.entity.MobCategory;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
@@ -68,6 +71,7 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||||
@@ -75,6 +79,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
private final ChunkGenerator delegate;
|
private final ChunkGenerator delegate;
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final CustomBiomeSource customBiomeSource;
|
private final CustomBiomeSource customBiomeSource;
|
||||||
|
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||||
private volatile Set<String> reachableStructureKeysCache;
|
private volatile Set<String> reachableStructureKeysCache;
|
||||||
|
|
||||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||||
@@ -225,7 +230,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||||
|
if (vanillaSpawnBiome == null) {
|
||||||
|
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype);
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = delegate.getMobsAt(
|
||||||
|
vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition);
|
||||||
|
if (resolvedSpawns != vanillaSpawns) {
|
||||||
|
return resolvedSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype);
|
||||||
|
if (explicitSpawns.isEmpty()) {
|
||||||
|
return vanillaSpawns;
|
||||||
|
}
|
||||||
|
if (vanillaSpawns.isEmpty()) {
|
||||||
|
return explicitSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||||
|
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -352,6 +378,24 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
delegate.spawnOriginalMobs(regionlimitedworldaccess);
|
delegate.spawnOriginalMobs(regionlimitedworldaccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||||
|
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
|
||||||
|
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
|
||||||
|
Set<EntityType<?>> explicitTypes = new HashSet<>();
|
||||||
|
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
|
||||||
|
explicitTypes.add(entry.value().type());
|
||||||
|
}
|
||||||
|
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
|
||||||
|
if (!explicitTypes.contains(entry.value().type())) {
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.addAll(explicitSpawns.unwrap());
|
||||||
|
return WeightedList.of(entries);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
|
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
|
||||||
return delegate.getSpawnHeight(levelheightaccessor);
|
return delegate.getSpawnHeight(levelheightaccessor);
|
||||||
@@ -428,4 +472,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -52,6 +52,7 @@ import net.minecraft.core.IdMapper;
|
|||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.core.Registry;
|
import net.minecraft.core.Registry;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.nbt.ByteTag;
|
import net.minecraft.nbt.ByteTag;
|
||||||
import net.minecraft.nbt.CollectionTag;
|
import net.minecraft.nbt.CollectionTag;
|
||||||
@@ -212,6 +213,16 @@ public class NMSBinding implements INMSBinding {
|
|||||||
return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material));
|
return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntitySpawnCategory(String key) {
|
||||||
|
Identifier identifier = Identifier.parse(key);
|
||||||
|
if (!BuiltInRegistries.ENTITY_TYPE.containsKey(identifier)) {
|
||||||
|
throw new IllegalArgumentException("Unknown native entity type: " + key);
|
||||||
|
}
|
||||||
|
EntityType<?> type = BuiltInRegistries.ENTITY_TYPE.getValue(identifier);
|
||||||
|
return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasTile(Location l) {
|
public boolean hasTile(Location l) {
|
||||||
return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null;
|
return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null;
|
||||||
|
|||||||
@@ -1221,7 +1221,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
if (!ff.exists()) {
|
if (!ff.exists()) {
|
||||||
ff.mkdirs();
|
ff.mkdirs();
|
||||||
service(StudioSVC.class).installIntoWorld(getSender(), dim.getLoadKey(), w.worldFolder());
|
dim = service(StudioSVC.class).installIntoWorld(getSender(), dim, w.worldFolder());
|
||||||
|
if (dim == null) {
|
||||||
|
throw new IllegalStateException("Failed to install dimension pack for " + id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||||
@@ -1243,9 +1246,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
for (String reason : result.getBlockingErrors()) {
|
for (String reason : result.getBlockingErrors()) {
|
||||||
Iris.error(" - " + reason);
|
Iris.error(" - " + reason);
|
||||||
}
|
}
|
||||||
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
|
} else if (!result.getWarnings().isEmpty()) {
|
||||||
Iris.info("Pack '" + result.getPackName() + "' validated ("
|
Iris.info("Pack '" + result.getPackName() + "' validated ("
|
||||||
+ result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, "
|
|
||||||
+ result.getWarnings().size() + " warning(s)).");
|
+ result.getWarnings().size() + " warning(s)).");
|
||||||
for (String warning : result.getWarnings()) {
|
for (String warning : result.getWarnings()) {
|
||||||
Iris.warn(" [" + result.getPackName() + "] " + warning);
|
Iris.warn(" [" + result.getPackName() + "] " + warning);
|
||||||
|
|||||||
+1
-1
@@ -218,7 +218,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
|
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder);
|
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Test")
|
@Director(description = "Test")
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
try {
|
try {
|
||||||
worldCreation = true;
|
worldCreation = true;
|
||||||
IrisToolbelt.createWorld()
|
IrisToolbelt.createWorld()
|
||||||
.dimension(dimension.getLoadKey())
|
.dimension(resolvedType)
|
||||||
.name(name)
|
.name(name)
|
||||||
.seed(seed)
|
.seed(seed)
|
||||||
.sender(sender())
|
.sender(sender())
|
||||||
@@ -199,7 +199,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
||||||
|
|
||||||
File worldFolder = new File(Bukkit.getWorldContainer(), name);
|
File worldFolder = new File(Bukkit.getWorldContainer(), name);
|
||||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension.getLoadKey(), worldFolder);
|
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
|
||||||
if (installed == null) {
|
if (installed == null) {
|
||||||
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
|
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
|
||||||
return false;
|
return false;
|
||||||
@@ -587,6 +587,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
IrisData data = IrisData.get(pack);
|
IrisData data = IrisData.get(pack);
|
||||||
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
||||||
options.add(key);
|
options.add(key);
|
||||||
|
options.add(pack.getName() + ":" + key);
|
||||||
}
|
}
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
|
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
|
||||||
|
|||||||
+120
-24
@@ -19,6 +19,8 @@
|
|||||||
package art.arcane.iris.core.commands;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
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.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.core.pack.PackValidationResult;
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
import art.arcane.iris.core.pack.PackValidator;
|
import art.arcane.iris.core.pack.PackValidator;
|
||||||
@@ -29,6 +31,7 @@ import art.arcane.volmlib.util.director.annotations.Director;
|
|||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
||||||
public class CommandPack implements DirectorExecutor {
|
public class CommandPack implements DirectorExecutor {
|
||||||
@@ -61,36 +64,115 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File target = new File(packsRoot, pack);
|
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||||
if (!target.isDirectory()) {
|
if (target == null) {
|
||||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runValidate(s, target);
|
runValidate(s, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r"})
|
@Director(description = "Preview or apply unused-resource cleanup", aliases = {"c"})
|
||||||
public void restore(
|
public void cleanup(
|
||||||
@Param(description = "The pack folder name to restore")
|
@Param(description = "The pack folder name to clean")
|
||||||
String pack
|
String pack,
|
||||||
|
@Param(description = "preview or apply", defaultValue = "preview")
|
||||||
|
String mode
|
||||||
) {
|
) {
|
||||||
VolmitSender s = sender();
|
VolmitSender s = sender();
|
||||||
if (pack == null || pack.isBlank()) {
|
File packFolder = findPack(s, pack);
|
||||||
s.sendMessage(C.RED + "You must specify a pack name.");
|
if (packFolder == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
File packFolder = new File(Iris.instance.getDataFolder("packs"), pack);
|
if ("apply".equalsIgnoreCase(mode)) {
|
||||||
if (!packFolder.isDirectory()) {
|
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
if (!result.success()) {
|
||||||
|
s.sendMessage(C.RED + result.error());
|
||||||
|
reportPaths(s, result.quarantinedPaths(), "still quarantined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.changed()) {
|
||||||
|
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.sendMessage(C.GREEN + "Quarantined " + result.quarantinedPaths().size()
|
||||||
|
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
|
||||||
|
reportPaths(s, result.quarantinedPaths(), "quarantined");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int restored = PackValidator.restoreTrash(packFolder);
|
if (!"preview".equalsIgnoreCase(mode)) {
|
||||||
if (restored == 0) {
|
s.sendMessage(C.RED + "Cleanup mode must be preview or apply.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
|
||||||
|
if (!preview.success()) {
|
||||||
|
s.sendMessage(C.RED + preview.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preview.hasCandidates()) {
|
||||||
|
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.sendMessage(C.YELLOW + "Cleanup preview for pack '" + pack + "': "
|
||||||
|
+ preview.candidatePaths().size() + " candidate(s). No files were changed.");
|
||||||
|
reportPaths(s, preview.candidatePaths(), "candidate");
|
||||||
|
s.sendMessage(C.GRAY + "Run /iris pack cleanup " + pack + " mode=apply to quarantine these candidates after a fresh scan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Preview or apply restoration of the latest quarantine", aliases = {"r"})
|
||||||
|
public void restore(
|
||||||
|
@Param(description = "The pack folder name to restore")
|
||||||
|
String pack,
|
||||||
|
@Param(description = "preview or apply", defaultValue = "preview")
|
||||||
|
String mode
|
||||||
|
) {
|
||||||
|
VolmitSender s = sender();
|
||||||
|
File packFolder = findPack(s, pack);
|
||||||
|
if (packFolder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("apply".equalsIgnoreCase(mode)) {
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||||
|
if (!result.conflicts().isEmpty()) {
|
||||||
|
s.sendMessage(C.RED + "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
|
||||||
|
reportPaths(s, result.conflicts(), "conflict");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.success()) {
|
||||||
|
s.sendMessage(C.RED + result.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.changed()) {
|
||||||
|
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.sendMessage(C.GREEN + "Restored " + result.restoredPaths().size()
|
||||||
|
+ " file(s) from " + result.dumpPath() + ".");
|
||||||
|
reportPaths(s, result.restoredPaths(), "restored");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!"preview".equalsIgnoreCase(mode)) {
|
||||||
|
s.sendMessage(C.RED + "Restore mode must be preview or apply.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
|
||||||
|
if (!preview.success()) {
|
||||||
|
s.sendMessage(C.RED + preview.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preview.hasFiles()) {
|
||||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.sendMessage(C.GREEN + "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
|
s.sendMessage(C.YELLOW + "Restore preview for " + preview.dumpPath() + ": "
|
||||||
s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check.");
|
+ preview.filePaths().size() + " file(s). No files were changed.");
|
||||||
|
reportPaths(s, preview.filePaths(), "file");
|
||||||
|
if (!preview.conflicts().isEmpty()) {
|
||||||
|
s.sendMessage(C.RED + "Restore is blocked by " + preview.conflicts().size() + " existing destination(s).");
|
||||||
|
reportPaths(s, preview.conflicts(), "conflict");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.sendMessage(C.GRAY + "Run /iris pack restore " + pack + " mode=apply to restore after a fresh conflict check.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
|
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
|
||||||
@@ -108,8 +190,7 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
|
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
|
||||||
s.sendMessage(tag + C.RESET + " " + name
|
s.sendMessage(tag + C.RESET + " " + name
|
||||||
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
|
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
|
||||||
+ ", warnings=" + result.getWarnings().size()
|
+ ", warnings=" + result.getWarnings().size() + ")");
|
||||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -137,8 +218,7 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
private void reportResult(VolmitSender s, PackValidationResult result) {
|
private void reportResult(VolmitSender s, PackValidationResult result) {
|
||||||
if (result.isLoadable()) {
|
if (result.isLoadable()) {
|
||||||
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
|
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
|
||||||
+ C.GRAY + " (warnings=" + result.getWarnings().size()
|
+ C.GRAY + " (warnings=" + result.getWarnings().size() + ")");
|
||||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
|
||||||
} else {
|
} else {
|
||||||
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
|
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
|
||||||
for (String reason : result.getBlockingErrors()) {
|
for (String reason : result.getBlockingErrors()) {
|
||||||
@@ -152,12 +232,28 @@ public class CommandPack implements DirectorExecutor {
|
|||||||
if (result.getWarnings().size() > wMax) {
|
if (result.getWarnings().size() > wMax) {
|
||||||
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
|
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
|
||||||
}
|
}
|
||||||
int tMax = Math.min(10, result.getRemovedUnusedFiles().size());
|
}
|
||||||
for (int i = 0; i < tMax; i++) {
|
|
||||||
s.sendMessage(C.GRAY + " ~ trashed " + result.getRemovedUnusedFiles().get(i));
|
private File findPack(VolmitSender sender, String pack) {
|
||||||
|
if (pack == null || pack.isBlank()) {
|
||||||
|
sender.sendMessage(C.RED + "You must specify a pack name.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (result.getRemovedUnusedFiles().size() > tMax) {
|
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
|
||||||
s.sendMessage(C.GRAY + " ... and " + (result.getRemovedUnusedFiles().size() - tMax) + " more trashed file(s).");
|
if (packFolder == null) {
|
||||||
|
sender.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return packFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportPaths(VolmitSender sender, List<String> paths, String label) {
|
||||||
|
int max = Math.min(10, paths.size());
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
sender.sendMessage(C.GRAY + " - " + label + ": " + paths.get(i));
|
||||||
|
}
|
||||||
|
if (paths.size() > max) {
|
||||||
|
sender.sendMessage(C.GRAY + " ... and " + (paths.size() - max) + " more.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -85,7 +85,9 @@ public class CommandPregen implements DirectorExecutor {
|
|||||||
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (PregeneratorJob.shutdownInstance()) {
|
if (PregeneratorJob.shutdownInstance()) {
|
||||||
Iris.info( C.BLUE + "Finishing up mca region...");
|
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
|
||||||
|
sender().sendMessage(message);
|
||||||
|
Iris.info(message);
|
||||||
} else {
|
} else {
|
||||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ sourceSets {
|
|||||||
srcDir '../modded-common/src/main/resources'
|
srcDir '../modded-common/src/main/resources'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
test {
|
||||||
|
java {
|
||||||
|
srcDir '../modded-common/src/test/java'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -102,6 +107,7 @@ configurations.create('jij') {
|
|||||||
dependencies {
|
dependencies {
|
||||||
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||||
|
testImplementation('junit:junit:4.13.2')
|
||||||
List<Object> fabricApi = [
|
List<Object> fabricApi = [
|
||||||
libs.fabricApi.base,
|
libs.fabricApi.base,
|
||||||
libs.fabricApi.registrySync,
|
libs.fabricApi.registrySync,
|
||||||
@@ -199,6 +205,10 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
exclude('META-INF/*.RSA')
|
exclude('META-INF/*.RSA')
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||||
|
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||||
|
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||||
|
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||||
|
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||||
exclude('oshi.properties')
|
exclude('oshi.properties')
|
||||||
from(project.configurations.named('jij')) {
|
from(project.configurations.named('jij')) {
|
||||||
into('META-INF/jars')
|
into('META-INF/jars')
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
exclude('org/apache/commons/lang/enum/**')
|
exclude('org/apache/commons/lang/enum/**')
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||||
|
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.LongSupplier;
|
||||||
|
|
||||||
|
final class InitialSpawnQueue {
|
||||||
|
private static final long DEFAULT_MAX_AGE_NANOS = TimeUnit.MINUTES.toNanos(5);
|
||||||
|
|
||||||
|
private final int capacity;
|
||||||
|
private final long maxAgeNanos;
|
||||||
|
private final LongSupplier nanoTime;
|
||||||
|
private final ArrayDeque<Long> queue;
|
||||||
|
private final Map<Long, Long> pending;
|
||||||
|
private final Set<Long> queued;
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
InitialSpawnQueue(int capacity) {
|
||||||
|
this(capacity, DEFAULT_MAX_AGE_NANOS, System::nanoTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
InitialSpawnQueue(int capacity, long maxAgeNanos, LongSupplier nanoTime) {
|
||||||
|
if (capacity < 1) {
|
||||||
|
throw new IllegalArgumentException("capacity must be positive");
|
||||||
|
}
|
||||||
|
if (maxAgeNanos < 1L) {
|
||||||
|
throw new IllegalArgumentException("maxAgeNanos must be positive");
|
||||||
|
}
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.maxAgeNanos = maxAgeNanos;
|
||||||
|
this.nanoTime = nanoTime;
|
||||||
|
this.queue = new ArrayDeque<>(Math.min(capacity, 256));
|
||||||
|
this.pending = new HashMap<>();
|
||||||
|
this.queued = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized boolean offer(long key) {
|
||||||
|
if (closed || pending.containsKey(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pending.size() >= capacity) {
|
||||||
|
expire(nanoTime.getAsLong());
|
||||||
|
}
|
||||||
|
if (pending.size() >= capacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pending.put(key, nanoTime.getAsLong());
|
||||||
|
queued.add(key);
|
||||||
|
queue.addLast(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized int batchSize(int limit) {
|
||||||
|
if (closed || limit <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(limit, queue.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized Long poll() {
|
||||||
|
long now = nanoTime.getAsLong();
|
||||||
|
Long key;
|
||||||
|
while ((key = queue.pollFirst()) != null) {
|
||||||
|
queued.remove(key);
|
||||||
|
Long offeredAt = pending.get(key);
|
||||||
|
if (offeredAt == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (expired(offeredAt, now)) {
|
||||||
|
pending.remove(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void retry(long key) {
|
||||||
|
Long offeredAt = pending.get(key);
|
||||||
|
if (closed || offeredAt == null || expired(offeredAt, nanoTime.getAsLong())) {
|
||||||
|
pending.remove(key);
|
||||||
|
queued.remove(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!queued.add(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.addLast(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void complete(long key) {
|
||||||
|
pending.remove(key);
|
||||||
|
if (queued.remove(key)) {
|
||||||
|
queue.removeFirstOccurrence(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized boolean isEmpty() {
|
||||||
|
return queue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized int size() {
|
||||||
|
return pending.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void clear() {
|
||||||
|
queue.clear();
|
||||||
|
pending.clear();
|
||||||
|
queued.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void close() {
|
||||||
|
closed = true;
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expire(long now) {
|
||||||
|
Set<Long> expired = new HashSet<>();
|
||||||
|
for (Map.Entry<Long, Long> entry : pending.entrySet()) {
|
||||||
|
if (expired(entry.getValue(), now)) {
|
||||||
|
expired.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expired.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Long key : expired) {
|
||||||
|
pending.remove(key);
|
||||||
|
queued.remove(key);
|
||||||
|
}
|
||||||
|
queue.removeIf(expired::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean expired(long offeredAt, long now) {
|
||||||
|
return now - offeredAt >= maxAgeNanos;
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -20,6 +20,8 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
@@ -38,6 +40,8 @@ import net.minecraft.resources.ResourceKey;
|
|||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.WorldGenRegion;
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.entity.MobCategory;
|
||||||
import net.minecraft.world.level.ChunkPos;
|
import net.minecraft.world.level.ChunkPos;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
@@ -49,6 +53,7 @@ import net.minecraft.world.level.biome.BiomeManager;
|
|||||||
import net.minecraft.world.level.biome.BiomeResolver;
|
import net.minecraft.world.level.biome.BiomeResolver;
|
||||||
import net.minecraft.world.level.biome.BiomeSource;
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
import net.minecraft.world.level.biome.Climate;
|
import net.minecraft.world.level.biome.Climate;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
@@ -65,6 +70,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -136,8 +142,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
private final String defaultPack;
|
private final String defaultPack;
|
||||||
private final String defaultDimensionKey;
|
private final String defaultDimensionKey;
|
||||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||||
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
||||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||||
|
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||||
private volatile Engine engine;
|
private volatile Engine engine;
|
||||||
private volatile String activePack;
|
private volatile String activePack;
|
||||||
private volatile String activeDimensionKey;
|
private volatile String activeDimensionKey;
|
||||||
@@ -166,6 +175,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
this.announced.set(false);
|
this.announced.set(false);
|
||||||
this.biomeHolders.clear();
|
this.biomeHolders.clear();
|
||||||
this.missingBiomeWarnings.clear();
|
this.missingBiomeWarnings.clear();
|
||||||
|
resetVanillaSpawnBiomes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void unbindEngine() {
|
public synchronized void unbindEngine() {
|
||||||
@@ -177,6 +187,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
this.announced.set(false);
|
this.announced.set(false);
|
||||||
this.biomeHolders.clear();
|
this.biomeHolders.clear();
|
||||||
this.missingBiomeWarnings.clear();
|
this.missingBiomeWarnings.clear();
|
||||||
|
resetVanillaSpawnBiomes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void resetToDefault() {
|
public synchronized void resetToDefault() {
|
||||||
@@ -259,6 +270,82 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
public void onHotload() {
|
public void onHotload() {
|
||||||
biomeHolders.clear();
|
biomeHolders.clear();
|
||||||
missingBiomeWarnings.clear();
|
missingBiomeWarnings.clear();
|
||||||
|
resetVanillaSpawnBiomes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
||||||
|
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||||
|
biome, structureManager, category, pos);
|
||||||
|
if (resolvedSpawns != explicitSpawns) {
|
||||||
|
return resolvedSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
initializeVanillaSpawnBiomes(registry);
|
||||||
|
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
|
||||||
|
if (vanillaSpawnBiome == null) {
|
||||||
|
return explicitSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||||
|
if (explicitSpawns.isEmpty()) {
|
||||||
|
return vanillaSpawns;
|
||||||
|
}
|
||||||
|
if (vanillaSpawns.isEmpty()) {
|
||||||
|
return explicitSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
|
||||||
|
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||||
|
if (vanillaSpawnBiomesInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Engine current = engineOrNull();
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||||
|
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) {
|
||||||
|
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||||
|
if (vanillaHolder == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
|
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||||
|
if (customHolder != null) {
|
||||||
|
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vanillaSpawnBiomesInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Identifier identifier = Identifier.tryParse(key);
|
||||||
|
if (identifier == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
|
||||||
|
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void resetVanillaSpawnBiomes() {
|
||||||
|
vanillaSpawnBiomes.clear();
|
||||||
|
mergedSpawnTables.clear();
|
||||||
|
vanillaSpawnBiomesInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -491,4 +578,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
|
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
|
||||||
info.add("Iris dimension: " + dimensionKey);
|
info.add("Iris dimension: " + dimensionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.modded;
|
|||||||
import art.arcane.iris.spi.PlatformEntityType;
|
import art.arcane.iris.spi.PlatformEntityType;
|
||||||
import net.minecraft.world.entity.EntityType;
|
import net.minecraft.world.entity.EntityType;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public final class ModdedEntityType implements PlatformEntityType {
|
public final class ModdedEntityType implements PlatformEntityType {
|
||||||
@@ -51,6 +52,11 @@ public final class ModdedEntityType implements PlatformEntityType {
|
|||||||
return namespace;
|
return namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String spawnCategory() {
|
||||||
|
return type.getCategory().getSerializedName().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object nativeHandle() {
|
public Object nativeHandle() {
|
||||||
return type;
|
return type;
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ public final class ModdedStartup {
|
|||||||
for (String reason : result.getBlockingErrors()) {
|
for (String reason : result.getBlockingErrors()) {
|
||||||
LOGGER.error(" - {}", reason);
|
LOGGER.error(" - {}", reason);
|
||||||
}
|
}
|
||||||
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
|
} else if (!result.getWarnings().isEmpty()) {
|
||||||
LOGGER.info("Iris pack '{}' validated ({} unused file(s) quarantined to .iris-trash/, {} warning(s)).", result.getPackName(), result.getRemovedUnusedFiles().size(), result.getWarnings().size());
|
LOGGER.info("Iris pack '{}' validated ({} warning(s)).", result.getPackName(), result.getWarnings().size());
|
||||||
for (String warning : result.getWarnings()) {
|
for (String warning : result.getWarnings()) {
|
||||||
LOGGER.warn(" [{}] {}", result.getPackName(), warning);
|
LOGGER.warn(" [{}] {}", result.getPackName(), warning);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,49 +40,85 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class ModdedWorldCheck {
|
public final class ModdedWorldCheck {
|
||||||
|
private static final int EXIT_PASS = 0;
|
||||||
|
private static final int EXIT_FAILURE = 1;
|
||||||
|
private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L;
|
||||||
|
private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L;
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||||
|
private static final ProcessExit PROCESS_EXIT = Runtime.getRuntime()::exit;
|
||||||
|
|
||||||
private ModdedWorldCheck() {
|
private ModdedWorldCheck() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void schedule() {
|
public static void schedule() {
|
||||||
Thread thread = new Thread(ModdedWorldCheck::waitAndRun, "Iris World Check");
|
coordinatorThread(() -> waitAndRun(PROCESS_EXIT)).start();
|
||||||
thread.setDaemon(true);
|
|
||||||
thread.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void waitAndRun() {
|
static Thread coordinatorThread(Runnable coordinator) {
|
||||||
|
Thread thread = new Thread(coordinator, "Iris World Check");
|
||||||
|
thread.setDaemon(false);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void waitAndRun(ProcessExit processExit) {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
MinecraftServer server = null;
|
MinecraftServer server = null;
|
||||||
while (System.currentTimeMillis() - start < 600000L) {
|
boolean ready = false;
|
||||||
MinecraftServer candidate = ModdedEngineBootstrap.currentServer();
|
int exitCode = EXIT_FAILURE;
|
||||||
if (candidate != null && candidate.isReady()) {
|
try {
|
||||||
server = candidate;
|
while (System.currentTimeMillis() - start < SERVER_WAIT_TIMEOUT_MILLIS) {
|
||||||
break;
|
MinecraftServer candidate = ModdedEngineBootstrap.currentServer();
|
||||||
|
if (candidate != null) {
|
||||||
|
server = candidate;
|
||||||
|
if (candidate.isReady()) {
|
||||||
|
ready = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.sleep(SERVER_WAIT_INTERVAL_MILLIS);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
Thread.sleep(250L);
|
if (!ready) {
|
||||||
} catch (InterruptedException e) {
|
LOGGER.error("[worldcheck] server did not become ready within 10 minutes");
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (server == null) {
|
MinecraftServer serverRef = server;
|
||||||
LOGGER.error("[worldcheck] server did not become ready within 10 minutes");
|
AtomicBoolean pass = new AtomicBoolean(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MinecraftServer serverRef = server;
|
|
||||||
AtomicBoolean pass = new AtomicBoolean(false);
|
|
||||||
try {
|
|
||||||
serverRef.submit(() -> pass.set(run(serverRef))).join();
|
serverRef.submit(() -> pass.set(run(serverRef))).join();
|
||||||
|
exitCode = pass.get() ? EXIT_PASS : EXIT_FAILURE;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOGGER.error("[worldcheck] coordinator interrupted", e);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LOGGER.error("[worldcheck] check failed", e);
|
LOGGER.error("[worldcheck] check failed", e);
|
||||||
|
} finally {
|
||||||
|
int resultCode = exitCode;
|
||||||
|
LOGGER.info("[worldcheck] shutting down dev server (result={})", resultCode == EXIT_PASS ? "PASS" : "FAIL");
|
||||||
|
MinecraftServer serverRef = server;
|
||||||
|
stopAndExit(serverRef == null ? null : () -> serverRef.halt(true), resultCode, processExit);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.info("[worldcheck] shutting down dev server (result={})", pass.get() ? "PASS" : "FAIL");
|
static void stopAndExit(Runnable serverStop, int requestedExitCode, ProcessExit processExit) {
|
||||||
serverRef.halt(false);
|
int exitCode = requestedExitCode;
|
||||||
|
boolean interrupted = Thread.interrupted();
|
||||||
|
if (interrupted) {
|
||||||
|
exitCode = EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (serverStop != null) {
|
||||||
|
serverStop.run();
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
exitCode = EXIT_FAILURE;
|
||||||
|
LOGGER.error("[worldcheck] server shutdown failed", e);
|
||||||
|
} finally {
|
||||||
|
if (interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processExit.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean run(MinecraftServer server) {
|
private static boolean run(MinecraftServer server) {
|
||||||
@@ -186,4 +222,9 @@ public final class ModdedWorldCheck {
|
|||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ProcessExit {
|
||||||
|
void exit(int status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-29
@@ -57,88 +57,167 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
|||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class ModdedWorldManager implements EngineWorldManager {
|
public final class ModdedWorldManager implements EngineWorldManager {
|
||||||
private static final ConcurrentHashMap<Engine, Queue<Long>> INITIAL_QUEUES = new ConcurrentHashMap<>();
|
|
||||||
private static final int MAX_INITIAL_QUEUE = 8192;
|
private static final int MAX_INITIAL_QUEUE = 8192;
|
||||||
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
|
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
|
||||||
|
private static final int MAX_INITIAL_RECOVERY_PER_PASS = 128;
|
||||||
|
private static final int MANTLE_WARMUP_QUEUE_CAPACITY = 256;
|
||||||
|
private static final long INITIAL_RECOVERY_INTERVAL_MS = 1_000L;
|
||||||
private static final long COUNT_INTERVAL_MS = 3_000L;
|
private static final long COUNT_INTERVAL_MS = 3_000L;
|
||||||
private static final int ENTITY_SCAN_RADIUS = 64;
|
private static final int ENTITY_SCAN_RADIUS = 64;
|
||||||
private static final int PLAYER_CHUNK_RADIUS = 4;
|
private static final int PLAYER_CHUNK_RADIUS = 4;
|
||||||
private static final int MIN_TICK_INTERVAL_MS = 1_000;
|
private static final int MIN_TICK_INTERVAL_MS = 1_000;
|
||||||
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
|
private final InitialSpawnQueue initialSpawnQueue;
|
||||||
|
private final Set<Long> mantleWarmups;
|
||||||
|
private final ThreadPoolExecutor mantleWarmupExecutor;
|
||||||
private long lastAmbientAt;
|
private long lastAmbientAt;
|
||||||
private long lastCountAt;
|
private long lastCountAt;
|
||||||
|
private long lastInitialRecoveryAt;
|
||||||
|
private volatile boolean closed;
|
||||||
private volatile int cachedEntityCount;
|
private volatile int cachedEntityCount;
|
||||||
private volatile int cachedConsideredChunks;
|
private volatile int cachedConsideredChunks;
|
||||||
private volatile double cachedSaturation;
|
private volatile double cachedSaturation;
|
||||||
|
|
||||||
public ModdedWorldManager(Engine engine) {
|
public ModdedWorldManager(Engine engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
INITIAL_QUEUES.putIfAbsent(engine, new ConcurrentLinkedQueue<>());
|
this.initialSpawnQueue = new InitialSpawnQueue(MAX_INITIAL_QUEUE);
|
||||||
|
this.mantleWarmups = ConcurrentHashMap.newKeySet();
|
||||||
|
BlockingQueue<Runnable> warmupQueue = new ArrayBlockingQueue<>(MANTLE_WARMUP_QUEUE_CAPACITY);
|
||||||
|
this.mantleWarmupExecutor = new ThreadPoolExecutor(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
30L,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
warmupQueue,
|
||||||
|
runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Initial Spawn Mantle Warmup");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
},
|
||||||
|
new ThreadPoolExecutor.AbortPolicy());
|
||||||
|
this.mantleWarmupExecutor.allowCoreThreadTimeOut(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) {
|
public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) {
|
||||||
if (engine == null) {
|
if (engine == null || engine.isClosed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
EngineWorldManager worldManager = engine.getWorldManager();
|
||||||
if (queue == null || queue.size() >= MAX_INITIAL_QUEUE) {
|
if (worldManager instanceof ModdedWorldManager moddedWorldManager) {
|
||||||
return;
|
moddedWorldManager.initialSpawnQueue.offer(pack(chunkX, chunkZ));
|
||||||
}
|
}
|
||||||
queue.add(pack(chunkX, chunkZ));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void serverTick(ServerLevel level) {
|
public void serverTick(ServerLevel level) {
|
||||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
if (closed || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPregenActive()) {
|
if (isPregenActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
recoverLoadedInitialSpawns(level);
|
||||||
drainInitialSpawns(level);
|
drainInitialSpawns(level);
|
||||||
ambientTick(level);
|
ambientTick(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drainInitialSpawns(ServerLevel level) {
|
private void recoverLoadedInitialSpawns(ServerLevel level) {
|
||||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
|
||||||
if (queue == null || queue.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||||
queue.clear();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - lastInitialRecoveryAt < INITIAL_RECOVERY_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastInitialRecoveryAt = now;
|
||||||
|
|
||||||
int budget = MAX_INITIAL_DRAIN_PER_TICK;
|
Set<Long> candidates = new HashSet<>();
|
||||||
while (budget-- > 0) {
|
for (ServerPlayer player : level.players()) {
|
||||||
Long key = queue.poll();
|
int centerX = player.blockPosition().getX() >> 4;
|
||||||
if (key == null) {
|
int centerZ = player.blockPosition().getZ() >> 4;
|
||||||
return;
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
candidates.add(pack(centerX + dx, centerZ + dz));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
candidates.addAll(level.getForceLoadedChunks());
|
||||||
|
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
|
int recovered = 0;
|
||||||
|
for (long key : candidates) {
|
||||||
int chunkX = unpackX(key);
|
int chunkX = unpackX(key);
|
||||||
int chunkZ = unpackZ(key);
|
int chunkZ = unpackZ(key);
|
||||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
if (mantle.isChunkLoaded(chunkX, chunkZ) && mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||||
initialSpawnChunk(level, chunkX, chunkZ);
|
continue;
|
||||||
} catch (Throwable e) {
|
}
|
||||||
IrisLogging.reportError(e);
|
if (initialSpawnQueue.offer(key) && ++recovered >= MAX_INITIAL_RECOVERY_PER_PASS) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
private void drainInitialSpawns(ServerLevel level) {
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
if (initialSpawnQueue.isEmpty()) {
|
||||||
if (!mantle.isChunkLoaded(chunkX, chunkZ) || mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||||
|
initialSpawnQueue.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int budget = initialSpawnQueue.batchSize(MAX_INITIAL_DRAIN_PER_TICK);
|
||||||
|
while (budget-- > 0) {
|
||||||
|
Long key = initialSpawnQueue.poll();
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int chunkX = unpackX(key);
|
||||||
|
int chunkZ = unpackZ(key);
|
||||||
|
boolean retry = false;
|
||||||
|
try {
|
||||||
|
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||||
|
retry = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!initialSpawnChunk(level, chunkX, chunkZ)) {
|
||||||
|
retry = true;
|
||||||
|
warmupMantleChunkAsync(key, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
retry = true;
|
||||||
|
} finally {
|
||||||
|
if (retry) {
|
||||||
|
initialSpawnQueue.retry(key);
|
||||||
|
} else {
|
||||||
|
initialSpawnQueue.complete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
|
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||||
try {
|
try {
|
||||||
@@ -153,6 +232,33 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
} finally {
|
} finally {
|
||||||
chunk.release();
|
chunk.release();
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warmupMantleChunkAsync(long key, int chunkX, int chunkZ) {
|
||||||
|
if (closed || !mantleWarmups.add(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mantleWarmupExecutor.execute(() -> warmupMantleChunk(key, chunkX, chunkZ));
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
mantleWarmups.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warmupMantleChunk(long key, int chunkX, int chunkZ) {
|
||||||
|
try {
|
||||||
|
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||||
|
if (!closed && !engine.isClosed() && !mantle.isClosed() && !mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
mantle.getChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (!closed && !engine.isClosed()) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
mantleWarmups.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ambientTick(ServerLevel level) {
|
private void ambientTick(ServerLevel level) {
|
||||||
@@ -575,7 +681,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
INITIAL_QUEUES.remove(engine);
|
closed = true;
|
||||||
|
initialSpawnQueue.close();
|
||||||
|
mantleWarmupExecutor.shutdownNow();
|
||||||
|
mantleWarmups.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import net.minecraft.util.random.Weighted;
|
||||||
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.entity.EntityType;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class NativeSpawnTableMerger {
|
||||||
|
private NativeSpawnTableMerger() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static WeightedList<MobSpawnSettings.SpawnerData> merge(
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||||
|
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
|
||||||
|
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
|
||||||
|
Set<EntityType<?>> explicitTypes = new HashSet<>();
|
||||||
|
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
|
||||||
|
explicitTypes.add(entry.value().type());
|
||||||
|
}
|
||||||
|
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
|
||||||
|
if (!explicitTypes.contains(entry.value().type())) {
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.addAll(explicitSpawns.unwrap());
|
||||||
|
return WeightedList.of(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -131,7 +131,8 @@ final class ModdedCommandHelp {
|
|||||||
SECTIONS.put("s", SECTIONS.get("studio"));
|
SECTIONS.put("s", SECTIONS.get("studio"));
|
||||||
SECTIONS.put("pack", List.of(
|
SECTIONS.put("pack", List.of(
|
||||||
Entry.command("validate", "[pack]", "Validate a pack or every pack", "v"),
|
Entry.command("validate", "[pack]", "Validate a pack or every pack", "v"),
|
||||||
Entry.command("restore", "<pack>", "Restore the latest trashed files for a pack", "r"),
|
Entry.command("cleanup", "<pack> [apply]", "Preview or quarantine unused-resource candidates", "c"),
|
||||||
|
Entry.command("restore", "<pack> [apply]", "Preview or restore the latest quarantine", "r"),
|
||||||
Entry.command("status", "[pack]", "Show cached validation status", "s")
|
Entry.command("status", "[pack]", "Show cached validation status", "s")
|
||||||
));
|
));
|
||||||
SECTIONS.put("pk", SECTIONS.get("pack"));
|
SECTIONS.put("pk", SECTIONS.get("pack"));
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ public final class ModdedGoldenHash {
|
|||||||
File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile();
|
File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile();
|
||||||
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
|
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
|
||||||
engine.getWorld().name(),
|
engine.getWorld().name(),
|
||||||
level.getSeed(),
|
engine.getSeedManager().getSeed(),
|
||||||
ModdedEngineBootstrap.loader().minecraftVersion(),
|
ModdedEngineBootstrap.loader().minecraftVersion(),
|
||||||
engine.getMinHeight(),
|
engine.getMinHeight(),
|
||||||
engine.getMaxHeight(),
|
engine.getMaxHeight(),
|
||||||
@@ -88,7 +88,7 @@ public final class ModdedGoldenHash {
|
|||||||
int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1);
|
int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1);
|
||||||
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + Math.max(1, threads) + " mode=" + mode);
|
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + Math.max(1, threads) + " mode=" + mode);
|
||||||
LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}",
|
LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}",
|
||||||
engine.getDimension().getLoadKey(), level.getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
|
engine.getDimension().getLoadKey(), engine.getSeedManager().getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
|
||||||
Thread thread = new Thread(() -> {
|
Thread thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
scan.hashEngine.run();
|
scan.hashEngine.run();
|
||||||
|
|||||||
+134
-21
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
|
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.core.pack.PackValidationResult;
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
import art.arcane.iris.core.pack.PackValidator;
|
import art.arcane.iris.core.pack.PackValidator;
|
||||||
@@ -58,12 +60,27 @@ public final class ModdedPackCommands {
|
|||||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
.executes((CommandContext<CommandSourceStack> context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
||||||
|
|
||||||
|
root.then(Commands.literal("cleanup")
|
||||||
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||||
|
.then(Commands.literal("apply")
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||||
|
root.then(Commands.literal("c")
|
||||||
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||||
|
.then(Commands.literal("apply")
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> cleanup(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||||
|
|
||||||
root.then(Commands.literal("restore")
|
root.then(Commands.literal("restore")
|
||||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||||
|
.then(Commands.literal("apply")
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||||
root.then(Commands.literal("r")
|
root.then(Commands.literal("r")
|
||||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack")))));
|
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), false))
|
||||||
|
.then(Commands.literal("apply")
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"), true)))));
|
||||||
|
|
||||||
root.then(Commands.literal("status")
|
root.then(Commands.literal("status")
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource(), null))
|
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource(), null))
|
||||||
@@ -99,8 +116,8 @@ public final class ModdedPackCommands {
|
|||||||
targets.add(dir);
|
targets.add(dir);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
File target = new File(packsRoot, pack);
|
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||||
if (!target.isDirectory()) {
|
if (target == null) {
|
||||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot.getAbsolutePath());
|
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot.getAbsolutePath());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -133,19 +150,45 @@ public final class ModdedPackCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int restore(CommandSourceStack source, String pack) {
|
private static int cleanup(CommandSourceStack source, String pack, boolean apply) {
|
||||||
File packFolder = new File(packsRoot(), pack);
|
File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack);
|
||||||
if (!packFolder.isDirectory()) {
|
if (packFolder == null) {
|
||||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
|
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int restored = PackValidator.restoreTrash(packFolder);
|
MinecraftServer server = source.getServer();
|
||||||
if (restored == 0) {
|
Thread thread = new Thread(() -> {
|
||||||
IrisModdedCommands.fail(source, "Nothing to restore for pack '" + pack + "'.");
|
if (apply) {
|
||||||
|
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||||
|
server.execute(() -> reportCleanupApply(source, pack, result));
|
||||||
|
} else {
|
||||||
|
PackResourceCleanup.Preview result = PackResourceCleanup.preview(packFolder);
|
||||||
|
server.execute(() -> reportCleanupPreview(source, pack, result));
|
||||||
|
}
|
||||||
|
}, "Iris Pack Cleanup");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int restore(CommandSourceStack source, String pack, boolean apply) {
|
||||||
|
File packFolder = PackDirectoryResolver.resolveExisting(packsRoot(), pack);
|
||||||
|
if (packFolder == null) {
|
||||||
|
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisModdedCommands.ok(source, "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
|
MinecraftServer server = source.getServer();
|
||||||
IrisModdedCommands.ok(source, "Re-run /iris pack validate " + pack + " to re-check.");
|
Thread thread = new Thread(() -> {
|
||||||
|
if (apply) {
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||||
|
server.execute(() -> reportRestoreApply(source, pack, result));
|
||||||
|
} else {
|
||||||
|
PackResourceCleanup.RestorePreview result = PackResourceCleanup.previewRestore(packFolder);
|
||||||
|
server.execute(() -> reportRestorePreview(source, pack, result));
|
||||||
|
}
|
||||||
|
}, "Iris Pack Restore");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +204,7 @@ public final class ModdedPackCommands {
|
|||||||
String tag = result.isLoadable() ? "OK" : "BROKEN";
|
String tag = result.isLoadable() ? "OK" : "BROKEN";
|
||||||
IrisModdedCommands.ok(source, tag + " " + entry.getKey()
|
IrisModdedCommands.ok(source, tag + " " + entry.getKey()
|
||||||
+ " (blocking=" + result.getBlockingErrors().size()
|
+ " (blocking=" + result.getBlockingErrors().size()
|
||||||
+ ", warnings=" + result.getWarnings().size()
|
+ ", warnings=" + result.getWarnings().size() + ")");
|
||||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -178,8 +220,7 @@ public final class ModdedPackCommands {
|
|||||||
private static void report(CommandSourceStack source, PackValidationResult result) {
|
private static void report(CommandSourceStack source, PackValidationResult result) {
|
||||||
if (result.isLoadable()) {
|
if (result.isLoadable()) {
|
||||||
IrisModdedCommands.ok(source, "Pack '" + result.getPackName() + "' is loadable."
|
IrisModdedCommands.ok(source, "Pack '" + result.getPackName() + "' is loadable."
|
||||||
+ " (warnings=" + result.getWarnings().size()
|
+ " (warnings=" + result.getWarnings().size() + ")");
|
||||||
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
|
|
||||||
} else {
|
} else {
|
||||||
IrisModdedCommands.fail(source, "Pack '" + result.getPackName() + "' is BROKEN:");
|
IrisModdedCommands.fail(source, "Pack '" + result.getPackName() + "' is BROKEN:");
|
||||||
for (String reason : result.getBlockingErrors()) {
|
for (String reason : result.getBlockingErrors()) {
|
||||||
@@ -193,12 +234,84 @@ public final class ModdedPackCommands {
|
|||||||
if (result.getWarnings().size() > warningMax) {
|
if (result.getWarnings().size() > warningMax) {
|
||||||
IrisModdedCommands.ok(source, " ... and " + (result.getWarnings().size() - warningMax) + " more warning(s).");
|
IrisModdedCommands.ok(source, " ... and " + (result.getWarnings().size() - warningMax) + " more warning(s).");
|
||||||
}
|
}
|
||||||
int trashMax = Math.min(10, result.getRemovedUnusedFiles().size());
|
}
|
||||||
for (int i = 0; i < trashMax; i++) {
|
|
||||||
IrisModdedCommands.ok(source, " ~ trashed " + result.getRemovedUnusedFiles().get(i));
|
private static void reportCleanupPreview(CommandSourceStack source, String pack, PackResourceCleanup.Preview result) {
|
||||||
|
if (!result.success()) {
|
||||||
|
IrisModdedCommands.fail(source, result.error());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (result.getRemovedUnusedFiles().size() > trashMax) {
|
if (!result.hasCandidates()) {
|
||||||
IrisModdedCommands.ok(source, " ... and " + (result.getRemovedUnusedFiles().size() - trashMax) + " more trashed file(s).");
|
IrisModdedCommands.ok(source, "No cleanup candidates found for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, "Cleanup preview for pack '" + pack + "': "
|
||||||
|
+ result.candidatePaths().size() + " candidate(s). No files were changed.");
|
||||||
|
reportPaths(source, result.candidatePaths(), "candidate");
|
||||||
|
IrisModdedCommands.ok(source, "Run /iris pack cleanup " + pack + " apply to quarantine after a fresh scan.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportCleanupApply(CommandSourceStack source, String pack, PackResourceCleanup.ApplyResult result) {
|
||||||
|
if (!result.success()) {
|
||||||
|
IrisModdedCommands.fail(source, result.error());
|
||||||
|
reportPaths(source, result.quarantinedPaths(), "still quarantined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.changed()) {
|
||||||
|
IrisModdedCommands.ok(source, "No cleanup candidates found for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, "Quarantined " + result.quarantinedPaths().size()
|
||||||
|
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
|
||||||
|
reportPaths(source, result.quarantinedPaths(), "quarantined");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportRestorePreview(CommandSourceStack source, String pack, PackResourceCleanup.RestorePreview result) {
|
||||||
|
if (!result.success()) {
|
||||||
|
IrisModdedCommands.fail(source, result.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.hasFiles()) {
|
||||||
|
IrisModdedCommands.ok(source, "Nothing to restore for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, "Restore preview for " + result.dumpPath() + ": "
|
||||||
|
+ result.filePaths().size() + " file(s). No files were changed.");
|
||||||
|
reportPaths(source, result.filePaths(), "file");
|
||||||
|
if (!result.conflicts().isEmpty()) {
|
||||||
|
IrisModdedCommands.fail(source, "Restore is blocked by " + result.conflicts().size() + " existing destination(s).");
|
||||||
|
reportPaths(source, result.conflicts(), "conflict");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, "Run /iris pack restore " + pack + " apply to restore after a fresh conflict check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportRestoreApply(CommandSourceStack source, String pack, PackResourceCleanup.RestoreResult result) {
|
||||||
|
if (!result.conflicts().isEmpty()) {
|
||||||
|
IrisModdedCommands.fail(source, "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
|
||||||
|
reportPaths(source, result.conflicts(), "conflict");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.success()) {
|
||||||
|
IrisModdedCommands.fail(source, result.error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!result.changed()) {
|
||||||
|
IrisModdedCommands.ok(source, "Nothing to restore for pack '" + pack + "'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisModdedCommands.ok(source, "Restored " + result.restoredPaths().size()
|
||||||
|
+ " file(s) from " + result.dumpPath() + ".");
|
||||||
|
reportPaths(source, result.restoredPaths(), "restored");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportPaths(CommandSourceStack source, List<String> paths, String label) {
|
||||||
|
int max = Math.min(10, paths.size());
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
IrisModdedCommands.ok(source, " - " + label + ": " + paths.get(i));
|
||||||
|
}
|
||||||
|
if (paths.size() > max) {
|
||||||
|
IrisModdedCommands.ok(source, " ... and " + (paths.size() - max) + " more.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -77,7 +77,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
this.maxInFlight = Math.max(8, pregen.getModdedPregenInFlight());
|
this.maxInFlight = Math.max(8, pregen.getModdedPregenInFlight());
|
||||||
this.minInFlight = Math.max(4, Math.min(16, maxInFlight / 4));
|
this.minInFlight = Math.max(4, Math.min(16, maxInFlight / 4));
|
||||||
this.semaphore = new Semaphore(maxInFlight, true);
|
this.semaphore = new Semaphore(maxInFlight, true);
|
||||||
this.adaptiveLimit = new AtomicInteger(maxInFlight);
|
this.adaptiveLimit = new AtomicInteger(sync ? 1 : maxInFlight);
|
||||||
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
||||||
this.backpressure = new PregenMantleBackpressure(
|
this.backpressure = new PregenMantleBackpressure(
|
||||||
this::getMantle,
|
this::getMantle,
|
||||||
@@ -179,6 +179,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
CompletableFuture<?> loadFuture = CompletableFuture
|
CompletableFuture<?> loadFuture = CompletableFuture
|
||||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
|
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
|
||||||
.thenCompose((CompletableFuture<?> inner) -> inner);
|
.thenCompose((CompletableFuture<?> inner) -> inner);
|
||||||
|
markSubmitted();
|
||||||
try {
|
try {
|
||||||
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
||||||
@@ -196,6 +197,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
|
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
|
||||||
listener.onChunkFailed(x, z);
|
listener.onChunkFailed(x, z);
|
||||||
} finally {
|
} finally {
|
||||||
|
markFinished();
|
||||||
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
|
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +257,9 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
private void markFinished() {
|
private void markFinished() {
|
||||||
inFlight.decrementAndGet();
|
inFlight.decrementAndGet();
|
||||||
|
if (sync) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
synchronized (permitMonitor) {
|
synchronized (permitMonitor) {
|
||||||
permitMonitor.notifyAll();
|
permitMonitor.notifyAll();
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -44,6 +44,7 @@ import net.minecraft.nbt.NbtUtils;
|
|||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
@@ -108,17 +109,22 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (level.players().isEmpty() || isPregenActive(engine)) {
|
if (!hasUpdateTargets(!level.players().isEmpty(), !level.getForceLoadedChunks().isEmpty()) || isPregenActive(engine)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateNearPlayers(engine, level);
|
updateNearPlayers(engine, level);
|
||||||
|
updateForcedChunks(engine, level);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean hasUpdateTargets(boolean hasPlayers, boolean hasForcedChunks) {
|
||||||
|
return hasPlayers || hasForcedChunks;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isPregenActive(Engine engine) {
|
private boolean isPregenActive(Engine engine) {
|
||||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||||
@@ -136,6 +142,12 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateForcedChunks(Engine engine, ServerLevel level) {
|
||||||
|
for (long chunkKey : level.getForceLoadedChunks()) {
|
||||||
|
updateChunk(engine, level, ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
for (int z = -1; z <= 1; z++) {
|
for (int z = -1; z <= 1; z++) {
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class InitialSpawnQueueTest {
|
||||||
|
@Test
|
||||||
|
public void rejectsDuplicatesAndEnforcesCapacityWhileInFlight() {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||||
|
|
||||||
|
assertTrue(queue.offer(1L));
|
||||||
|
assertFalse(queue.offer(1L));
|
||||||
|
assertTrue(queue.offer(2L));
|
||||||
|
assertEquals(Long.valueOf(1L), queue.poll());
|
||||||
|
assertFalse(queue.offer(3L));
|
||||||
|
queue.complete(1L);
|
||||||
|
assertTrue(queue.offer(3L));
|
||||||
|
assertEquals(2, queue.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retryRotatesWithoutBlockingReadyEntries() {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(4);
|
||||||
|
queue.offer(1L);
|
||||||
|
queue.offer(2L);
|
||||||
|
queue.offer(3L);
|
||||||
|
|
||||||
|
int budget = queue.batchSize(8);
|
||||||
|
Long unavailable = queue.poll();
|
||||||
|
queue.retry(unavailable);
|
||||||
|
Long firstReady = queue.poll();
|
||||||
|
queue.complete(firstReady);
|
||||||
|
Long secondReady = queue.poll();
|
||||||
|
queue.complete(secondReady);
|
||||||
|
|
||||||
|
assertEquals(3, budget);
|
||||||
|
assertEquals(Long.valueOf(1L), unavailable);
|
||||||
|
assertEquals(Long.valueOf(2L), firstReady);
|
||||||
|
assertEquals(Long.valueOf(3L), secondReady);
|
||||||
|
assertEquals(1, queue.batchSize(8));
|
||||||
|
assertEquals(Long.valueOf(1L), queue.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retryIsDeduplicated() {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||||
|
queue.offer(7L);
|
||||||
|
Long key = queue.poll();
|
||||||
|
|
||||||
|
queue.retry(key);
|
||||||
|
queue.retry(key);
|
||||||
|
|
||||||
|
assertEquals(1, queue.batchSize(8));
|
||||||
|
assertEquals(Long.valueOf(7L), queue.poll());
|
||||||
|
assertNull(queue.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeClearsAndRejectsFurtherWork() {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||||
|
queue.offer(1L);
|
||||||
|
|
||||||
|
queue.close();
|
||||||
|
|
||||||
|
assertTrue(queue.isEmpty());
|
||||||
|
assertEquals(0, queue.size());
|
||||||
|
assertFalse(queue.offer(2L));
|
||||||
|
assertEquals(0, queue.batchSize(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clearAllowsLaterWork() {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2);
|
||||||
|
queue.offer(1L);
|
||||||
|
|
||||||
|
queue.clear();
|
||||||
|
|
||||||
|
assertTrue(queue.offer(2L));
|
||||||
|
assertEquals(Long.valueOf(2L), queue.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void expiredEntriesReleaseCapacity() {
|
||||||
|
AtomicLong now = new AtomicLong();
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get);
|
||||||
|
queue.offer(1L);
|
||||||
|
queue.offer(2L);
|
||||||
|
|
||||||
|
now.set(100L);
|
||||||
|
|
||||||
|
assertTrue(queue.offer(3L));
|
||||||
|
assertEquals(1, queue.size());
|
||||||
|
assertEquals(Long.valueOf(3L), queue.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void expiredInFlightEntryCannotRetry() {
|
||||||
|
AtomicLong now = new AtomicLong();
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(2, 100L, now::get);
|
||||||
|
queue.offer(1L);
|
||||||
|
Long inFlight = queue.poll();
|
||||||
|
|
||||||
|
now.set(100L);
|
||||||
|
queue.retry(inFlight);
|
||||||
|
|
||||||
|
assertEquals(0, queue.size());
|
||||||
|
assertNull(queue.poll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentOffersRemainDeduplicatedAndBounded() throws Exception {
|
||||||
|
InitialSpawnQueue queue = new InitialSpawnQueue(256);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||||
|
List<Future<?>> futures = new ArrayList<>();
|
||||||
|
for (int thread = 0; thread < 8; thread++) {
|
||||||
|
futures.add(executor.submit(() -> {
|
||||||
|
for (long key = 0L; key < 512L; key++) {
|
||||||
|
queue.offer(key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (Future<?> future : futures) {
|
||||||
|
future.get();
|
||||||
|
}
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
|
assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS));
|
||||||
|
assertEquals(256, queue.size());
|
||||||
|
assertEquals(256, queue.batchSize(512));
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.util.random.Weighted;
|
||||||
|
import net.minecraft.util.random.WeightedList;
|
||||||
|
import net.minecraft.world.entity.EntityTypes;
|
||||||
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class IrisModdedChunkGeneratorSpawnTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void explicitSpawnsReplaceMatchingVanillaTypesAndPreserveOthers() {
|
||||||
|
MobSpawnSettings.SpawnerData zombie = new MobSpawnSettings.SpawnerData(EntityTypes.ZOMBIE, 1, 4);
|
||||||
|
MobSpawnSettings.SpawnerData vanillaSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 1, 1);
|
||||||
|
MobSpawnSettings.SpawnerData explicitSlime = new MobSpawnSettings.SpawnerData(EntityTypes.SLIME, 2, 5);
|
||||||
|
MobSpawnSettings.SpawnerData explicitCow = new MobSpawnSettings.SpawnerData(EntityTypes.COW, 2, 4);
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> vanilla = WeightedList.of(List.of(
|
||||||
|
new Weighted<>(zombie, 100),
|
||||||
|
new Weighted<>(vanillaSlime, 1)));
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> explicit = WeightedList.of(List.of(
|
||||||
|
new Weighted<>(explicitSlime, 7),
|
||||||
|
new Weighted<>(explicitCow, 3)));
|
||||||
|
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> merged = NativeSpawnTableMerger.merge(vanilla, explicit);
|
||||||
|
List<Weighted<MobSpawnSettings.SpawnerData>> entries = merged.unwrap();
|
||||||
|
|
||||||
|
assertEquals(3, entries.size());
|
||||||
|
assertEquals(EntityTypes.ZOMBIE, entries.get(0).value().type());
|
||||||
|
assertEquals(100, entries.get(0).weight());
|
||||||
|
assertEquals(explicitSlime, entries.get(1).value());
|
||||||
|
assertEquals(7, entries.get(1).weight());
|
||||||
|
assertEquals(explicitCow, entries.get(2).value());
|
||||||
|
assertEquals(3, entries.get(2).weight());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedWorldCheckTest {
|
||||||
|
@Test
|
||||||
|
public void coordinatorThreadIsNonDaemon() {
|
||||||
|
Thread thread = ModdedWorldCheck.coordinatorThread(() -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertFalse(thread.isDaemon());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void passStopsServerBeforeZeroExit() {
|
||||||
|
List<String> events = new ArrayList<>();
|
||||||
|
|
||||||
|
ModdedWorldCheck.stopAndExit(
|
||||||
|
() -> events.add("stop"),
|
||||||
|
0,
|
||||||
|
status -> events.add("exit:" + status)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(List.of("stop", "exit:0"), events);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failureStopsServerBeforeNonzeroExit() {
|
||||||
|
List<String> events = new ArrayList<>();
|
||||||
|
|
||||||
|
ModdedWorldCheck.stopAndExit(
|
||||||
|
() -> events.add("stop"),
|
||||||
|
1,
|
||||||
|
status -> events.add("exit:" + status)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(List.of("stop", "exit:1"), events);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownFailureForcesNonzeroExit() {
|
||||||
|
AtomicInteger status = new AtomicInteger(-1);
|
||||||
|
|
||||||
|
ModdedWorldCheck.stopAndExit(
|
||||||
|
() -> {
|
||||||
|
throw new IllegalStateException("shutdown failed");
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
status::set
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(1, status.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interruptionIsClearedForShutdownAndRestoredBeforeExit() {
|
||||||
|
AtomicBoolean interruptedDuringStop = new AtomicBoolean(true);
|
||||||
|
AtomicBoolean interruptedDuringExit = new AtomicBoolean(false);
|
||||||
|
AtomicInteger status = new AtomicInteger(-1);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
try {
|
||||||
|
ModdedWorldCheck.stopAndExit(
|
||||||
|
() -> interruptedDuringStop.set(Thread.currentThread().isInterrupted()),
|
||||||
|
0,
|
||||||
|
exitStatus -> {
|
||||||
|
status.set(exitStatus);
|
||||||
|
interruptedDuringExit.set(Thread.currentThread().isInterrupted());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(interruptedDuringStop.get());
|
||||||
|
assertTrue(interruptedDuringExit.get());
|
||||||
|
assertEquals(1, status.get());
|
||||||
|
} finally {
|
||||||
|
Thread.interrupted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package art.arcane.iris.modded.service;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedChunkUpdateServiceTest {
|
||||||
|
@Test
|
||||||
|
public void scansWhenPlayersArePresent() {
|
||||||
|
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scansHeadlessForceLoadedChunks() {
|
||||||
|
assertTrue(ModdedChunkUpdateService.hasUpdateTargets(false, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void skipsLevelsWithoutPlayersOrForcedChunks() {
|
||||||
|
assertFalse(ModdedChunkUpdateService.hasUpdateTargets(false, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,6 +189,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
exclude('META-INF/*.RSA')
|
exclude('META-INF/*.RSA')
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||||
|
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
./gradlew buildAll --no-parallel -PuseLocalVolmLib=false "$@"
|
./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@"
|
||||||
|
|||||||
+62
-125
@@ -71,36 +71,14 @@ String bukkitArtifactName = irisArtifactName('CraftBukkit', minecraftVersion)
|
|||||||
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
||||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
||||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
||||||
extensions.extraProperties.set('bukkitArtifactName', bukkitArtifactName)
|
|
||||||
extensions.extraProperties.set('fabricArtifactName', fabricArtifactName)
|
|
||||||
extensions.extraProperties.set('forgeArtifactName', forgeArtifactName)
|
|
||||||
extensions.extraProperties.set('neoForgeArtifactName', neoForgeArtifactName)
|
|
||||||
|
|
||||||
apply plugin: ApiGenerator
|
apply plugin: ApiGenerator
|
||||||
|
|
||||||
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
|
String consumerLocation = providers.gradleProperty('location')
|
||||||
// ======================== WINDOWS =============================
|
.getOrElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers')
|
||||||
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
|
String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins"
|
||||||
registerCustomOutputTask('Psycho', 'C://Dan/MinecraftDevelopment/Server/plugins')
|
String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods"
|
||||||
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
|
String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods"
|
||||||
registerCustomOutputTask('Coco', 'D://mcsm/plugins')
|
String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods"
|
||||||
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
|
|
||||||
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19.4/plugins')
|
|
||||||
registerCustomOutputTask('CrazyDev22', 'C://Users/Julian/Desktop/server/plugins')
|
|
||||||
registerCustomOutputTask('PixelFury', 'C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins')
|
|
||||||
registerCustomOutputTask('PixelFuryDev', 'C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins')
|
|
||||||
// ========================== UNIX ==============================
|
|
||||||
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
|
|
||||||
registerCustomOutputTaskUnix(
|
|
||||||
'PsychoLT',
|
|
||||||
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins',
|
|
||||||
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/fabric-mod-consumers/dropins/mods',
|
|
||||||
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/forge-mod-consumers/dropins/mods',
|
|
||||||
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/neoforge-mod-consumers/dropins/mods'
|
|
||||||
)
|
|
||||||
registerCustomOutputTaskUnix('PixelMac', '/Users/test/Desktop/mcserver/plugins')
|
|
||||||
registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugins')
|
|
||||||
// ==============================================================
|
|
||||||
|
|
||||||
def nmsBindings = [
|
def nmsBindings = [
|
||||||
v26_2_R1: '26.2.build.25-alpha',
|
v26_2_R1: '26.2.build.25-alpha',
|
||||||
@@ -272,14 +250,14 @@ tasks.named('neoforgeJar').configure {
|
|||||||
mustRunAfter(tasks.named('forgeJar'))
|
mustRunAfter(tasks.named('forgeJar'))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('buildAll') {
|
tasks.register('buildAllToOut') {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar')
|
dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar')
|
||||||
doLast {
|
doLast {
|
||||||
File dist = layout.projectDirectory.dir('dist').asFile
|
File dist = layout.projectDirectory.dir('dist').asFile
|
||||||
File[] jars = dist.listFiles((java.io.FileFilter) { File f -> f.name.endsWith('.jar') })
|
File[] jars = dist.listFiles((java.io.FileFilter) { File f -> f.name.endsWith('.jar') })
|
||||||
println('')
|
println('')
|
||||||
println('=== Iris buildAll -> dist/ ===')
|
println('=== Iris buildAllToOut -> dist/ ===')
|
||||||
if (jars != null) {
|
if (jars != null) {
|
||||||
jars.sort { it.name }.each { File f ->
|
jars.sort { it.name }.each { File f ->
|
||||||
println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024)))
|
println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024)))
|
||||||
@@ -288,6 +266,60 @@ tasks.register('buildAll') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('buildAll') {
|
||||||
|
group = 'iris'
|
||||||
|
outputs.upToDateWhen { false }
|
||||||
|
dependsOn('iris', 'fabricJar', 'forgeJar', 'neoforgeJar')
|
||||||
|
doLast {
|
||||||
|
delete(
|
||||||
|
file("${bukkitConsumerPath}/Iris-Fabric.jar"),
|
||||||
|
file("${bukkitConsumerPath}/Iris-Forge.jar"),
|
||||||
|
file("${bukkitConsumerPath}/Iris-Neoforge.jar")
|
||||||
|
)
|
||||||
|
copy {
|
||||||
|
from(layout.buildDirectory.file(bukkitArtifactName))
|
||||||
|
into(bukkitConsumerPath)
|
||||||
|
rename { String ignored -> 'Iris.jar' }
|
||||||
|
}
|
||||||
|
delete(
|
||||||
|
file("${fabricConsumerPath}/Iris.jar"),
|
||||||
|
file("${fabricConsumerPath}/Iris-Forge.jar"),
|
||||||
|
file("${fabricConsumerPath}/Iris-Neoforge.jar")
|
||||||
|
)
|
||||||
|
copy {
|
||||||
|
from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}"))
|
||||||
|
into(fabricConsumerPath)
|
||||||
|
rename { String ignored -> 'Iris-Fabric.jar' }
|
||||||
|
}
|
||||||
|
delete(
|
||||||
|
file("${forgeConsumerPath}/Iris.jar"),
|
||||||
|
file("${forgeConsumerPath}/Iris-Fabric.jar"),
|
||||||
|
file("${forgeConsumerPath}/Iris-Neoforge.jar")
|
||||||
|
)
|
||||||
|
copy {
|
||||||
|
from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}"))
|
||||||
|
into(forgeConsumerPath)
|
||||||
|
rename { String ignored -> 'Iris-Forge.jar' }
|
||||||
|
}
|
||||||
|
delete(
|
||||||
|
file("${neoForgeConsumerPath}/Iris.jar"),
|
||||||
|
file("${neoForgeConsumerPath}/Iris-Fabric.jar"),
|
||||||
|
file("${neoForgeConsumerPath}/Iris-Forge.jar")
|
||||||
|
)
|
||||||
|
copy {
|
||||||
|
from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}"))
|
||||||
|
into(neoForgeConsumerPath)
|
||||||
|
rename { String ignored -> 'Iris-Neoforge.jar' }
|
||||||
|
}
|
||||||
|
println('')
|
||||||
|
println("=== Iris buildAll -> ${consumerLocation} ===")
|
||||||
|
println(" ${bukkitConsumerPath}/Iris.jar")
|
||||||
|
println(" ${fabricConsumerPath}/Iris-Fabric.jar")
|
||||||
|
println(" ${forgeConsumerPath}/Iris-Forge.jar")
|
||||||
|
println(" ${neoForgeConsumerPath}/Iris-Neoforge.jar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('irisDev', Copy) {
|
tasks.register('irisDev', Copy) {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar'))
|
from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar'))
|
||||||
@@ -409,98 +441,3 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
|
|||||||
System.err.println()
|
System.err.println()
|
||||||
System.exit(69)
|
System.exit(69)
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerCustomOutputTask(String name, String path) {
|
|
||||||
if (!System.getProperty('os.name').toLowerCase().contains('windows')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createCustomOutputTasks(name, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
void registerCustomOutputTask(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
|
|
||||||
if (!System.getProperty('os.name').toLowerCase().contains('windows')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createCustomOutputTasks(name, path, fabricPath, forgePath, neoForgePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
void registerCustomOutputTaskUnix(String name, String path) {
|
|
||||||
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createCustomOutputTasks(name, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
void registerCustomOutputTaskUnix(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
|
|
||||||
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createCustomOutputTasks(name, path, fabricPath, forgePath, neoForgePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
void createCustomOutputTasks(String name, String path) {
|
|
||||||
createBukkitOutputTask(name, path)
|
|
||||||
|
|
||||||
tasks.register("buildAll${name}") {
|
|
||||||
group = 'development'
|
|
||||||
dependsOn("build${name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void createCustomOutputTasks(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
|
|
||||||
createBukkitOutputTask(name, path)
|
|
||||||
createPlatformOutputTask(name, fabricPath, 'Fabric', 'buildFabric', "adapters/fabric/build/libs/${String.valueOf(project.ext.fabricArtifactName)}")
|
|
||||||
createPlatformOutputTask(name, forgePath, 'Forge', 'buildForge', "adapters/forge/build/libs/${String.valueOf(project.ext.forgeArtifactName)}")
|
|
||||||
createPlatformOutputTask(name, neoForgePath, 'Neoforge', 'buildNeoforge', "adapters/neoforge/build/libs/${String.valueOf(project.ext.neoForgeArtifactName)}")
|
|
||||||
|
|
||||||
tasks.register("buildAll${name}") {
|
|
||||||
group = 'development'
|
|
||||||
dependsOn(
|
|
||||||
"build${name}",
|
|
||||||
"build${name}Fabric",
|
|
||||||
"build${name}Forge",
|
|
||||||
"build${name}Neoforge"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void createBukkitOutputTask(String name, String path) {
|
|
||||||
tasks.register("build${name}", Copy) {
|
|
||||||
group = 'development'
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
dependsOn('iris')
|
|
||||||
from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName))
|
|
||||||
into(file(path))
|
|
||||||
rename { String ignored -> 'Iris.jar' }
|
|
||||||
doFirst {
|
|
||||||
delete(
|
|
||||||
file("${path}/Iris-Fabric.jar"),
|
|
||||||
file("${path}/Iris-Forge.jar"),
|
|
||||||
file("${path}/Iris-Neoforge.jar")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void createPlatformOutputTask(String name, String path, String platform, String buildTask, String sourcePath) {
|
|
||||||
tasks.register("build${name}${platform}", Copy) {
|
|
||||||
group = 'development'
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
dependsOn(buildTask)
|
|
||||||
from(layout.projectDirectory.file(sourcePath))
|
|
||||||
into(file(path))
|
|
||||||
rename { String ignored -> "Iris-${platform}.jar" }
|
|
||||||
doFirst {
|
|
||||||
delete(
|
|
||||||
file("${path}/Iris.jar"),
|
|
||||||
file("${path}/Iris-Fabric.jar"),
|
|
||||||
file("${path}/Iris-Forge.jar"),
|
|
||||||
file("${path}/Iris-Neoforge.jar")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
DimensionHeight height = new DimensionHeight(fixer);
|
DimensionHeight height = new DimensionHeight(fixer);
|
||||||
KList<File> folders = getDatapacksFolder();
|
KList<File> folders = getDatapacksFolder();
|
||||||
|
IrisDimension.clearGeneratedBiomeTags(folders);
|
||||||
DatapackIngestService.reapplyFromStaging(folders);
|
DatapackIngestService.reapplyFromStaging(folders);
|
||||||
java.util.concurrent.ConcurrentMap<String, KSet<String>> biomes = new java.util.concurrent.ConcurrentHashMap<>();
|
java.util.concurrent.ConcurrentMap<String, KSet<String>> biomes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,9 @@ public interface INMSBinding {
|
|||||||
void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException;
|
void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException;
|
||||||
|
|
||||||
Vector3d getBoundingbox(org.bukkit.entity.EntityType entity);
|
Vector3d getBoundingbox(org.bukkit.entity.EntityType entity);
|
||||||
|
|
||||||
|
String getEntitySpawnCategory(String key);
|
||||||
|
|
||||||
Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason);
|
Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason);
|
||||||
|
|
||||||
Color getBiomeColor(Location location, BiomeColor type);
|
Color getBiomeColor(Location location, BiomeColor type);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.OVERWORLD, """
|
Dimension.OVERWORLD, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.0,
|
"ambient_light": 0.0,
|
||||||
|
"default_clock": "minecraft:overworld",
|
||||||
"has_ender_dragon_fight": false,
|
"has_ender_dragon_fight": false,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:audio/ambient_sounds": {
|
"minecraft:audio/ambient_sounds": {
|
||||||
@@ -43,6 +44,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.NETHER, """
|
Dimension.NETHER, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.1,
|
"ambient_light": 0.1,
|
||||||
|
"has_fixed_time": true,
|
||||||
"has_ender_dragon_fight": false,
|
"has_ender_dragon_fight": false,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:gameplay/sky_light_level": 4.0,
|
"minecraft:gameplay/sky_light_level": 4.0,
|
||||||
@@ -59,6 +61,8 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.END, """
|
Dimension.END, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.25,
|
"ambient_light": 0.25,
|
||||||
|
"default_clock": "minecraft:the_end",
|
||||||
|
"has_fixed_time": true,
|
||||||
"has_ender_dragon_fight": true,
|
"has_ender_dragon_fight": true,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:audio/ambient_sounds": {
|
"minecraft:audio/ambient_sounds": {
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ public class NMSBinding1X implements INMSBinding {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntitySpawnCategory(String key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MCAPaletteAccess createPalette() {
|
public MCAPaletteAccess createPalette() {
|
||||||
IrisLogging.error("Cannot use the global data palette! Iris is incapable of using MCA generation on this version of minecraft!");
|
IrisLogging.error("Cannot use the global data palette! Iris is incapable of using MCA generation on this version of minecraft!");
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class PackDirectoryResolver {
|
||||||
|
private PackDirectoryResolver() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File resolveExisting(File packsRoot, String packName) {
|
||||||
|
if (packsRoot == null || packName == null || packName.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Path root = packsRoot.toPath().toAbsolutePath().normalize();
|
||||||
|
Path candidate = root.resolve(packName).normalize();
|
||||||
|
if (!root.equals(candidate.getParent())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Files.isSymbolicLink(candidate) || !Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return candidate.toFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,13 +30,18 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class PackDownloader {
|
public final class PackDownloader {
|
||||||
|
private static final Pattern GITHUB_REPOSITORY = Pattern.compile("[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+");
|
||||||
|
private static final Pattern GITHUB_REF = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._/-]*");
|
||||||
|
private static final Pattern COMMIT_SHA = Pattern.compile("[0-9a-fA-F]{40}");
|
||||||
|
|
||||||
private PackDownloader() {
|
private PackDownloader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String download(File packsFolder, String repo, String branch, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
|
public static String download(File packsFolder, String repo, String ref, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
|
||||||
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
|
String url = directUrl ? ref : resolveGithubArchiveUrl(repo, ref);
|
||||||
feedback.accept("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
|
feedback.accept("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
|
||||||
File zip = WebCache.getNonCachedFile("pack-" + repo, url);
|
File zip = WebCache.getNonCachedFile("pack-" + repo, url);
|
||||||
File temp = WebCache.getTemp();
|
File temp = WebCache.getTemp();
|
||||||
@@ -136,6 +141,57 @@ public final class PackDownloader {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String resolveGithubArchiveUrl(String repo, String ref) {
|
||||||
|
if (repo == null || !GITHUB_REPOSITORY.matcher(repo).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid GitHub repository '" + repo + "'");
|
||||||
|
}
|
||||||
|
String[] repositoryParts = repo.split("/", -1);
|
||||||
|
if (repositoryParts[0].equals(".") || repositoryParts[0].equals("..")
|
||||||
|
|| repositoryParts[1].equals(".") || repositoryParts[1].equals("..")) {
|
||||||
|
throw new IllegalArgumentException("Invalid GitHub repository '" + repo + "'");
|
||||||
|
}
|
||||||
|
if (ref == null || ref.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("GitHub reference cannot be empty");
|
||||||
|
}
|
||||||
|
if (COMMIT_SHA.matcher(ref).matches()) {
|
||||||
|
return "https://github.com/" + repo + "/archive/" + ref + ".zip";
|
||||||
|
}
|
||||||
|
if (ref.startsWith("refs/") && !ref.startsWith("refs/heads/") && !ref.startsWith("refs/tags/")) {
|
||||||
|
throw new IllegalArgumentException("Unsupported GitHub reference '" + ref + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
String qualifiedRef;
|
||||||
|
if (ref.startsWith("refs/heads/") || ref.startsWith("refs/tags/")) {
|
||||||
|
qualifiedRef = ref;
|
||||||
|
} else {
|
||||||
|
qualifiedRef = "refs/heads/" + ref;
|
||||||
|
}
|
||||||
|
validateGithubRef(qualifiedRef);
|
||||||
|
return "https://codeload.github.com/" + repo + "/zip/" + qualifiedRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateGithubRef(String qualifiedRef) {
|
||||||
|
String refPath = qualifiedRef.startsWith("refs/heads/")
|
||||||
|
? qualifiedRef.substring("refs/heads/".length())
|
||||||
|
: qualifiedRef.substring("refs/tags/".length());
|
||||||
|
if (!GITHUB_REF.matcher(refPath).matches()
|
||||||
|
|| refPath.contains("..")
|
||||||
|
|| refPath.contains("//")
|
||||||
|
|| refPath.contains("@{")
|
||||||
|
|| refPath.endsWith("/")
|
||||||
|
|| refPath.endsWith(".")
|
||||||
|
|| refPath.endsWith(".lock")) {
|
||||||
|
throw new IllegalArgumentException("Invalid GitHub reference '" + qualifiedRef + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] segments = refPath.split("/");
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment.startsWith(".") || segment.endsWith(".lock")) {
|
||||||
|
throw new IllegalArgumentException("Invalid GitHub reference '" + qualifiedRef + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateDownloaded(File packEntry, Consumer<String> feedback) {
|
private static void validateDownloaded(File packEntry, Consumer<String> feedback) {
|
||||||
try {
|
try {
|
||||||
PackValidationResult result = PackValidator.validate(packEntry);
|
PackValidationResult result = PackValidator.validate(packEntry);
|
||||||
@@ -146,9 +202,8 @@ public final class PackDownloader {
|
|||||||
for (String reason : result.getBlockingErrors()) {
|
for (String reason : result.getBlockingErrors()) {
|
||||||
feedback.accept(" - " + reason);
|
feedback.accept(" - " + reason);
|
||||||
}
|
}
|
||||||
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
|
} else if (!result.getWarnings().isEmpty()) {
|
||||||
feedback.accept("Pack '" + result.getPackName() + "' validated ("
|
feedback.accept("Pack '" + result.getPackName() + "' validated ("
|
||||||
+ result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, "
|
|
||||||
+ result.getWarnings().size() + " warning(s)).");
|
+ result.getWarnings().size() + " warning(s)).");
|
||||||
} else {
|
} else {
|
||||||
feedback.accept("Pack '" + result.getPackName() + "' validated.");
|
feedback.accept("Pack '" + result.getPackName() + "' validated.");
|
||||||
|
|||||||
@@ -0,0 +1,620 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public final class PackResourceCleanup {
|
||||||
|
private static final String TRASH_ROOT = ".iris-trash";
|
||||||
|
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
|
||||||
|
"biomes",
|
||||||
|
"regions",
|
||||||
|
"entities",
|
||||||
|
"spawners",
|
||||||
|
"loot",
|
||||||
|
"generators",
|
||||||
|
"expressions",
|
||||||
|
"markers",
|
||||||
|
"blocks",
|
||||||
|
"mods"
|
||||||
|
);
|
||||||
|
private static final List<String> CORPUS_EXCLUSIONS = List.of(
|
||||||
|
TRASH_ROOT,
|
||||||
|
"datapack-imports",
|
||||||
|
"externaldatapacks",
|
||||||
|
"internaldatapacks",
|
||||||
|
"datapacks",
|
||||||
|
"cache",
|
||||||
|
"objects",
|
||||||
|
".iris"
|
||||||
|
);
|
||||||
|
private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS");
|
||||||
|
private static final Map<Path, Object> PACK_LOCKS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private PackResourceCleanup() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Preview preview(File packFolder) {
|
||||||
|
Path lockPath = lockPath(packFolder);
|
||||||
|
if (lockPath == null) {
|
||||||
|
return new Preview(List.of(), "Pack folder is required.");
|
||||||
|
}
|
||||||
|
synchronized (packLock(lockPath)) {
|
||||||
|
try {
|
||||||
|
Path packRoot = requirePackRoot(packFolder);
|
||||||
|
CleanupScan scan = scanCleanup(packRoot);
|
||||||
|
return new Preview(scan.paths(), null);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return new Preview(List.of(), errorMessage("Unable to scan pack resources", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplyResult apply(File packFolder) {
|
||||||
|
Path lockPath = lockPath(packFolder);
|
||||||
|
if (lockPath == null) {
|
||||||
|
return new ApplyResult(null, List.of(), "Pack folder is required.");
|
||||||
|
}
|
||||||
|
synchronized (packLock(lockPath)) {
|
||||||
|
try {
|
||||||
|
Path packRoot = requirePackRoot(packFolder);
|
||||||
|
CleanupScan scan = scanCleanup(packRoot);
|
||||||
|
if (scan.paths().isEmpty()) {
|
||||||
|
return new ApplyResult(null, List.of(), null);
|
||||||
|
}
|
||||||
|
return quarantine(packRoot, scan.paths());
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return new ApplyResult(null, List.of(), errorMessage("Unable to quarantine pack resources", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RestorePreview previewRestore(File packFolder) {
|
||||||
|
Path lockPath = lockPath(packFolder);
|
||||||
|
if (lockPath == null) {
|
||||||
|
return new RestorePreview(null, List.of(), List.of(), "Pack folder is required.");
|
||||||
|
}
|
||||||
|
synchronized (packLock(lockPath)) {
|
||||||
|
try {
|
||||||
|
Path packRoot = requirePackRoot(packFolder);
|
||||||
|
RestoreScan scan = scanRestore(packRoot);
|
||||||
|
return new RestorePreview(scan.dumpPath(), scan.paths(), scan.conflicts(), null);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return new RestorePreview(null, List.of(), List.of(), errorMessage("Unable to scan quarantined resources", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RestoreResult restoreLatest(File packFolder) {
|
||||||
|
Path lockPath = lockPath(packFolder);
|
||||||
|
if (lockPath == null) {
|
||||||
|
return new RestoreResult(null, List.of(), List.of(), "Pack folder is required.");
|
||||||
|
}
|
||||||
|
synchronized (packLock(lockPath)) {
|
||||||
|
try {
|
||||||
|
Path packRoot = requirePackRoot(packFolder);
|
||||||
|
RestoreScan scan = scanRestore(packRoot);
|
||||||
|
if (scan.dump() == null) {
|
||||||
|
return new RestoreResult(null, List.of(), List.of(), null);
|
||||||
|
}
|
||||||
|
if (!scan.conflicts().isEmpty()) {
|
||||||
|
return new RestoreResult(scan.dumpPath(), List.of(), scan.conflicts(), null);
|
||||||
|
}
|
||||||
|
return restore(packRoot, scan);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return new RestoreResult(null, List.of(), List.of(), errorMessage("Unable to restore quarantined resources", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApplyResult quarantine(Path packRoot, List<String> paths) throws IOException {
|
||||||
|
Path dump = createUniqueDump(packRoot);
|
||||||
|
List<Transfer> moved = new ArrayList<>(paths.size());
|
||||||
|
try {
|
||||||
|
for (String path : paths) {
|
||||||
|
Path source = resolveContained(packRoot, path);
|
||||||
|
requireRegularFile(source, "Cleanup candidate");
|
||||||
|
Path destination = resolveContained(dump, path);
|
||||||
|
if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
throw new FileAlreadyExistsException(destination.toString());
|
||||||
|
}
|
||||||
|
Files.createDirectories(destination.getParent());
|
||||||
|
Files.move(source, destination);
|
||||||
|
moved.add(new Transfer(path, source, destination));
|
||||||
|
}
|
||||||
|
return new ApplyResult(relativePath(packRoot, dump), paths, null);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
List<String> remaining = rollbackMoves(moved);
|
||||||
|
deleteEmptyDirectories(dump);
|
||||||
|
deleteIfEmpty(dump.getParent());
|
||||||
|
String rollbackState = remaining.isEmpty()
|
||||||
|
? "Quarantine failed and was rolled back"
|
||||||
|
: "Quarantine failed and rollback left " + remaining.size() + " file(s) quarantined";
|
||||||
|
IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), e);
|
||||||
|
return new ApplyResult(relativePath(packRoot, dump), remaining, errorMessage(rollbackState, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestoreResult restore(Path packRoot, RestoreScan scan) throws IOException {
|
||||||
|
List<Path> copiedDestinations = new ArrayList<>(scan.transfers().size());
|
||||||
|
List<Path> createdDirectories = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (Transfer transfer : scan.transfers()) {
|
||||||
|
createDirectories(packRoot, transfer.destination().getParent(), createdDirectories);
|
||||||
|
Files.copy(transfer.source(), transfer.destination(), StandardCopyOption.COPY_ATTRIBUTES);
|
||||||
|
copiedDestinations.add(transfer.destination());
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
List<String> rollbackRemainders = rollbackCopies(packRoot, copiedDestinations, createdDirectories);
|
||||||
|
String rollbackState = rollbackRemainders.isEmpty()
|
||||||
|
? "Restore copy failed and was rolled back"
|
||||||
|
: "Restore copy failed and rollback left " + rollbackRemainders.size() + " destination(s)";
|
||||||
|
IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), e);
|
||||||
|
return new RestoreResult(scan.dumpPath(), rollbackRemainders, List.of(), errorMessage(rollbackState, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Transfer> removedSources = new ArrayList<>(scan.transfers().size());
|
||||||
|
for (Transfer transfer : scan.transfers()) {
|
||||||
|
try {
|
||||||
|
Files.delete(transfer.source());
|
||||||
|
removedSources.add(transfer);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return rollbackSourceRemoval(packRoot, scan, copiedDestinations, createdDirectories, removedSources, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteEmptyDirectories(scan.dump());
|
||||||
|
deleteIfEmpty(scan.dump().getParent());
|
||||||
|
return new RestoreResult(scan.dumpPath(), scan.paths(), List.of(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CleanupScan scanCleanup(Path packRoot) throws IOException {
|
||||||
|
List<Path> corpusFiles = listTree(packRoot);
|
||||||
|
StringBuilder corpus = new StringBuilder(1 << 16);
|
||||||
|
for (Path path : corpusFiles) {
|
||||||
|
if (!isScannableJson(packRoot, path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requireRegularFile(path, "Corpus file");
|
||||||
|
corpus.append(Files.readString(path, StandardCharsets.UTF_8)).append('\n');
|
||||||
|
}
|
||||||
|
if (corpus.isEmpty()) {
|
||||||
|
return new CleanupScan(List.of());
|
||||||
|
}
|
||||||
|
String corpusText = corpus.toString();
|
||||||
|
|
||||||
|
List<String> candidates = new ArrayList<>();
|
||||||
|
for (String folderName : MANAGED_RESOURCE_FOLDERS) {
|
||||||
|
Path resourceFolder = resolveContained(packRoot, folderName);
|
||||||
|
if (!Files.exists(resourceFolder, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requireDirectory(resourceFolder, "Managed resource folder");
|
||||||
|
for (Path resource : listTree(resourceFolder)) {
|
||||||
|
if (!isJsonFile(resource)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requireRegularFile(resource, "Managed resource");
|
||||||
|
String key = stripJsonExtension(relativePath(resourceFolder, resource));
|
||||||
|
if (!key.isBlank() && !isReferenced(corpusText, key)) {
|
||||||
|
candidates.add(relativePath(packRoot, resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.sort(String::compareTo);
|
||||||
|
return new CleanupScan(List.copyOf(candidates));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestoreScan scanRestore(Path packRoot) throws IOException {
|
||||||
|
Path trashRoot = resolveContained(packRoot, TRASH_ROOT);
|
||||||
|
if (!Files.exists(trashRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return RestoreScan.empty();
|
||||||
|
}
|
||||||
|
requireDirectory(trashRoot, "Quarantine root");
|
||||||
|
List<Path> dumps;
|
||||||
|
try (Stream<Path> stream = Files.list(trashRoot)) {
|
||||||
|
dumps = stream.sorted(Comparator.comparing(path -> path.getFileName().toString())).toList();
|
||||||
|
} catch (UncheckedIOException e) {
|
||||||
|
throw e.getCause();
|
||||||
|
}
|
||||||
|
List<Path> directories = new ArrayList<>();
|
||||||
|
for (Path dump : dumps) {
|
||||||
|
if (Files.isSymbolicLink(dump)) {
|
||||||
|
throw new IOException("Quarantine entry is a symbolic link: " + dump);
|
||||||
|
}
|
||||||
|
if (Files.isDirectory(dump, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
directories.add(dump);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (directories.isEmpty()) {
|
||||||
|
return RestoreScan.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Path latest = directories.get(directories.size() - 1);
|
||||||
|
String dumpPath = relativePath(packRoot, latest);
|
||||||
|
List<Transfer> transfers = new ArrayList<>();
|
||||||
|
List<String> conflicts = new ArrayList<>();
|
||||||
|
for (Path source : listTree(latest)) {
|
||||||
|
if (Files.isDirectory(source, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requireRegularFile(source, "Quarantined resource");
|
||||||
|
String relative = relativePath(latest, source);
|
||||||
|
Path destination = resolveContained(packRoot, relative);
|
||||||
|
if (destination.startsWith(trashRoot)) {
|
||||||
|
throw new IOException("Quarantined resource resolves inside the quarantine root: " + relative);
|
||||||
|
}
|
||||||
|
if (hasDestinationConflict(packRoot, destination)) {
|
||||||
|
conflicts.add(relative);
|
||||||
|
}
|
||||||
|
transfers.add(new Transfer(relative, source, destination));
|
||||||
|
}
|
||||||
|
transfers.sort(Comparator.comparing(Transfer::path));
|
||||||
|
conflicts.sort(String::compareTo);
|
||||||
|
List<String> paths = transfers.stream().map(Transfer::path).toList();
|
||||||
|
return new RestoreScan(latest, dumpPath, List.copyOf(transfers), List.copyOf(paths), List.copyOf(conflicts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Path> listTree(Path root) throws IOException {
|
||||||
|
requireDirectory(root, "Scan root");
|
||||||
|
try (Stream<Path> stream = Files.walk(root)) {
|
||||||
|
List<Path> paths = stream.sorted(Comparator.comparing(path -> relativePath(root, path))).toList();
|
||||||
|
for (Path path : paths) {
|
||||||
|
if (!path.equals(root) && Files.isSymbolicLink(path)) {
|
||||||
|
throw new IOException("Symbolic links are not supported during pack cleanup: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
} catch (UncheckedIOException e) {
|
||||||
|
throw e.getCause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path createUniqueDump(Path packRoot) throws IOException {
|
||||||
|
Path trashRoot = resolveContained(packRoot, TRASH_ROOT);
|
||||||
|
if (Files.exists(trashRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
requireDirectory(trashRoot, "Quarantine root");
|
||||||
|
} else {
|
||||||
|
Files.createDirectory(trashRoot);
|
||||||
|
}
|
||||||
|
String baseName = LocalDateTime.now().format(TRASH_STAMP);
|
||||||
|
for (int attempt = 0; attempt < 10_000; attempt++) {
|
||||||
|
String name = attempt == 0 ? baseName : baseName + '-' + String.format(Locale.ROOT, "%04d", attempt);
|
||||||
|
Path dump = resolveContained(trashRoot, name);
|
||||||
|
try {
|
||||||
|
return Files.createDirectory(dump);
|
||||||
|
} catch (FileAlreadyExistsException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("Unable to allocate a unique quarantine directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> rollbackMoves(List<Transfer> moved) {
|
||||||
|
for (int i = moved.size() - 1; i >= 0; i--) {
|
||||||
|
Transfer transfer = moved.get(i);
|
||||||
|
try {
|
||||||
|
if (!Files.exists(transfer.source(), LinkOption.NOFOLLOW_LINKS)
|
||||||
|
&& Files.exists(transfer.destination(), LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
Files.move(transfer.destination(), transfer.source());
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
IrisLogging.reportError("Failed to roll back quarantined resource '" + transfer.path() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> remaining = new ArrayList<>();
|
||||||
|
for (Transfer transfer : moved) {
|
||||||
|
if (Files.exists(transfer.destination(), LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
remaining.add(transfer.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remaining.sort(String::compareTo);
|
||||||
|
return List.copyOf(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestoreResult rollbackSourceRemoval(Path packRoot,
|
||||||
|
RestoreScan scan,
|
||||||
|
List<Path> copiedDestinations,
|
||||||
|
List<Path> createdDirectories,
|
||||||
|
List<Transfer> removedSources,
|
||||||
|
Throwable failure) {
|
||||||
|
List<String> sourceRollbackFailures = new ArrayList<>();
|
||||||
|
for (int i = removedSources.size() - 1; i >= 0; i--) {
|
||||||
|
Transfer transfer = removedSources.get(i);
|
||||||
|
try {
|
||||||
|
Files.copy(transfer.destination(), transfer.source(), StandardCopyOption.COPY_ATTRIBUTES);
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
sourceRollbackFailures.add(transfer.path());
|
||||||
|
IrisLogging.reportError("Failed to restore quarantined source '" + transfer.path() + "' during rollback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Path> removableDestinations = new ArrayList<>();
|
||||||
|
for (Path destination : copiedDestinations) {
|
||||||
|
String path = relativePath(packRoot, destination);
|
||||||
|
Path source = scan.dump().resolve(path).normalize();
|
||||||
|
if (Files.exists(source, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
removableDestinations.add(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> destinationRemainders = rollbackCopies(packRoot, removableDestinations, createdDirectories);
|
||||||
|
List<String> remainders = new ArrayList<>(sourceRollbackFailures.size() + destinationRemainders.size());
|
||||||
|
remainders.addAll(sourceRollbackFailures);
|
||||||
|
for (String destinationRemainder : destinationRemainders) {
|
||||||
|
if (!remainders.contains(destinationRemainder)) {
|
||||||
|
remainders.add(destinationRemainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remainders.sort(String::compareTo);
|
||||||
|
String rollbackState = remainders.isEmpty()
|
||||||
|
? "Restore source removal failed and was rolled back"
|
||||||
|
: "Restore source removal failed and rollback left " + remainders.size() + " destination(s)";
|
||||||
|
IrisLogging.reportError(rollbackState + " for pack " + packRoot.getFileName(), failure);
|
||||||
|
return new RestoreResult(scan.dumpPath(), remainders, List.of(), errorMessage(rollbackState, failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> rollbackCopies(Path packRoot,
|
||||||
|
List<Path> destinations,
|
||||||
|
List<Path> createdDirectories) {
|
||||||
|
for (int i = destinations.size() - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(destinations.get(i));
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
IrisLogging.reportError("Failed to roll back restored resource '" + relativePath(packRoot, destinations.get(i)) + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = createdDirectories.size() - 1; i >= 0; i--) {
|
||||||
|
deleteIfEmpty(createdDirectories.get(i));
|
||||||
|
}
|
||||||
|
List<String> remaining = new ArrayList<>();
|
||||||
|
for (Path destination : destinations) {
|
||||||
|
if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
remaining.add(relativePath(packRoot, destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remaining.sort(String::compareTo);
|
||||||
|
return List.copyOf(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createDirectories(Path packRoot, Path target, List<Path> createdDirectories) throws IOException {
|
||||||
|
Path contained = requireContained(packRoot, target);
|
||||||
|
List<Path> missing = new ArrayList<>();
|
||||||
|
Path current = contained;
|
||||||
|
while (!current.equals(packRoot) && !Files.exists(current, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
missing.add(current);
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
if (!Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(current)) {
|
||||||
|
throw new IOException("Restore parent is not a safe directory: " + current);
|
||||||
|
}
|
||||||
|
for (int i = missing.size() - 1; i >= 0; i--) {
|
||||||
|
Path directory = missing.get(i);
|
||||||
|
Files.createDirectory(directory);
|
||||||
|
createdDirectories.add(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasDestinationConflict(Path packRoot, Path destination) throws IOException {
|
||||||
|
if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Path parent = destination.getParent();
|
||||||
|
while (parent != null && !parent.equals(packRoot)) {
|
||||||
|
if (Files.exists(parent, LinkOption.NOFOLLOW_LINKS)
|
||||||
|
&& (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(parent))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
parent = parent.getParent();
|
||||||
|
}
|
||||||
|
return parent == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteEmptyDirectories(Path root) {
|
||||||
|
if (root == null || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (Stream<Path> stream = Files.walk(root)) {
|
||||||
|
List<Path> directories = stream.filter(path -> Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS))
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.toList();
|
||||||
|
for (Path directory : directories) {
|
||||||
|
deleteIfEmpty(directory);
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteIfEmpty(Path directory) {
|
||||||
|
if (directory == null || !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (Stream<Path> stream = Files.list(directory)) {
|
||||||
|
if (stream.findAny().isEmpty()) {
|
||||||
|
Files.deleteIfExists(directory);
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isScannableJson(Path packRoot, Path path) {
|
||||||
|
if (!isJsonFile(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Path relative = packRoot.relativize(path);
|
||||||
|
for (Path segment : relative) {
|
||||||
|
if (CORPUS_EXCLUSIONS.contains(segment.toString())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isJsonFile(Path path) {
|
||||||
|
return path.getFileName() != null && path.getFileName().toString().endsWith(".json")
|
||||||
|
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReferenced(String corpus, String key) {
|
||||||
|
if (corpus.contains("\"" + key + "\"")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int slash = key.indexOf('/');
|
||||||
|
if (slash <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String tail = key.substring(slash + 1);
|
||||||
|
return !tail.isBlank() && corpus.contains("\"" + tail + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripJsonExtension(String path) {
|
||||||
|
return path.substring(0, path.length() - ".json".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path requirePackRoot(File packFolder) throws IOException {
|
||||||
|
if (packFolder == null) {
|
||||||
|
throw new IOException("Pack folder is required.");
|
||||||
|
}
|
||||||
|
Path root = packFolder.toPath().toAbsolutePath().normalize();
|
||||||
|
requireDirectory(root, "Pack folder");
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireDirectory(Path path, String label) throws IOException {
|
||||||
|
if (Files.isSymbolicLink(path) || !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
throw new IOException(label + " is not a safe directory: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireRegularFile(Path path, String label) throws IOException {
|
||||||
|
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
throw new IOException(label + " is not a safe regular file: " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path resolveContained(Path root, String relative) throws IOException {
|
||||||
|
return requireContained(root, root.resolve(relative).normalize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path requireContained(Path root, Path path) throws IOException {
|
||||||
|
Path normalizedRoot = root.toAbsolutePath().normalize();
|
||||||
|
Path normalizedPath = path.toAbsolutePath().normalize();
|
||||||
|
if (!normalizedPath.startsWith(normalizedRoot)) {
|
||||||
|
throw new IOException("Path escapes pack boundary: " + path);
|
||||||
|
}
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String relativePath(Path root, Path path) {
|
||||||
|
return root.relativize(path).toString().replace(File.separatorChar, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path lockPath(File packFolder) {
|
||||||
|
return packFolder == null ? null : packFolder.toPath().toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object packLock(Path path) {
|
||||||
|
return PACK_LOCKS.computeIfAbsent(path, ignored -> new Object());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String errorMessage(String prefix, Throwable error) {
|
||||||
|
String message = error.getMessage();
|
||||||
|
return prefix + (message == null || message.isBlank() ? "." : ": " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Preview(List<String> candidatePaths, String error) {
|
||||||
|
public Preview {
|
||||||
|
candidatePaths = candidatePaths == null ? List.of() : List.copyOf(candidatePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return error == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasCandidates() {
|
||||||
|
return !candidatePaths.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyResult(String quarantinePath, List<String> quarantinedPaths, String error) {
|
||||||
|
public ApplyResult {
|
||||||
|
quarantinedPaths = quarantinedPaths == null ? List.of() : List.copyOf(quarantinedPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return error == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean changed() {
|
||||||
|
return !quarantinedPaths.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RestorePreview(String dumpPath, List<String> filePaths, List<String> conflicts, String error) {
|
||||||
|
public RestorePreview {
|
||||||
|
filePaths = filePaths == null ? List.of() : List.copyOf(filePaths);
|
||||||
|
conflicts = conflicts == null ? List.of() : List.copyOf(conflicts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return error == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFiles() {
|
||||||
|
return !filePaths.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canRestore() {
|
||||||
|
return success() && hasFiles() && conflicts.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasConflicts() {
|
||||||
|
return !conflicts.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RestoreResult(String dumpPath, List<String> restoredPaths, List<String> conflicts, String error) {
|
||||||
|
public RestoreResult {
|
||||||
|
restoredPaths = restoredPaths == null ? List.of() : List.copyOf(restoredPaths);
|
||||||
|
conflicts = conflicts == null ? List.of() : List.copyOf(conflicts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return error == null && conflicts.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean changed() {
|
||||||
|
return !restoredPaths.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasConflicts() {
|
||||||
|
return !conflicts.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CleanupScan(List<String> paths) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RestoreScan(Path dump, String dumpPath, List<Transfer> transfers, List<String> paths,
|
||||||
|
List<String> conflicts) {
|
||||||
|
private static RestoreScan empty() {
|
||||||
|
return new RestoreScan(null, null, List.of(), List.of(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Transfer(String path, Path source, Path destination) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,18 +26,15 @@ public final class PackValidationResult {
|
|||||||
private final String packName;
|
private final String packName;
|
||||||
private final List<String> blockingErrors;
|
private final List<String> blockingErrors;
|
||||||
private final List<String> warnings;
|
private final List<String> warnings;
|
||||||
private final List<String> removedUnusedFiles;
|
|
||||||
private final long validatedAtMillis;
|
private final long validatedAtMillis;
|
||||||
|
|
||||||
public PackValidationResult(String packName,
|
public PackValidationResult(String packName,
|
||||||
List<String> blockingErrors,
|
List<String> blockingErrors,
|
||||||
List<String> warnings,
|
List<String> warnings,
|
||||||
List<String> removedUnusedFiles,
|
|
||||||
long validatedAtMillis) {
|
long validatedAtMillis) {
|
||||||
this.packName = packName;
|
this.packName = packName;
|
||||||
this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors);
|
this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors);
|
||||||
this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings);
|
this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings);
|
||||||
this.removedUnusedFiles = removedUnusedFiles == null ? new ArrayList<>() : new ArrayList<>(removedUnusedFiles);
|
|
||||||
this.validatedAtMillis = validatedAtMillis;
|
this.validatedAtMillis = validatedAtMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +54,6 @@ public final class PackValidationResult {
|
|||||||
return Collections.unmodifiableList(warnings);
|
return Collections.unmodifiableList(warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getRemovedUnusedFiles() {
|
|
||||||
return Collections.unmodifiableList(removedUnusedFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getValidatedAtMillis() {
|
public long getValidatedAtMillis() {
|
||||||
return validatedAtMillis;
|
return validatedAtMillis;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.pack;
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformEntityType;
|
||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.volmlib.util.json.JSONArray;
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
@@ -28,17 +30,16 @@ import java.io.File;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class PackValidator {
|
public final class PackValidator {
|
||||||
@@ -50,19 +51,9 @@ public final class PackValidator {
|
|||||||
private static final String CACHE_FOLDER = "cache";
|
private static final String CACHE_FOLDER = "cache";
|
||||||
private static final String OBJECTS_FOLDER = "objects";
|
private static final String OBJECTS_FOLDER = "objects";
|
||||||
private static final String DIMENSIONS_FOLDER = "dimensions";
|
private static final String DIMENSIONS_FOLDER = "dimensions";
|
||||||
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
|
private static final List<String> STRUCTURE_HOST_FOLDERS = List.of(DIMENSIONS_FOLDER, "regions", "biomes");
|
||||||
"biomes",
|
private static final List<String> UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS = List.of("rotation", "translate", "scale");
|
||||||
"regions",
|
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||||
"entities",
|
|
||||||
"spawners",
|
|
||||||
"loot",
|
|
||||||
"generators",
|
|
||||||
"expressions",
|
|
||||||
"markers",
|
|
||||||
"blocks",
|
|
||||||
"mods"
|
|
||||||
);
|
|
||||||
private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
|
|
||||||
|
|
||||||
private PackValidator() {
|
private PackValidator() {
|
||||||
}
|
}
|
||||||
@@ -71,39 +62,376 @@ public final class PackValidator {
|
|||||||
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
|
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
|
||||||
List<String> blockingErrors = new ArrayList<>();
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
List<String> warnings = new ArrayList<>();
|
List<String> warnings = new ArrayList<>();
|
||||||
List<String> removedUnusedFiles = new ArrayList<>();
|
|
||||||
long validatedAt = System.currentTimeMillis();
|
long validatedAt = System.currentTimeMillis();
|
||||||
|
|
||||||
if (packFolder == null || !packFolder.isDirectory()) {
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
blockingErrors.add("Pack folder does not exist or is not a directory.");
|
blockingErrors.add("Pack folder does not exist or is not a directory.");
|
||||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
||||||
if (!dimensionsFolder.isDirectory()) {
|
if (!dimensionsFolder.isDirectory()) {
|
||||||
blockingErrors.add("Missing dimensions/ folder.");
|
blockingErrors.add("Missing dimensions/ folder.");
|
||||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
||||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||||
blockingErrors.add("No dimension JSON files under dimensions/.");
|
blockingErrors.add("No dimension JSON files under dimensions/.");
|
||||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||||
|
blockingErrors.addAll(validateUnsupportedStructureTransforms(packFolder));
|
||||||
try {
|
blockingErrors.addAll(validateSpawnerEntityReferences(
|
||||||
String packTextCorpus = buildPackTextCorpus(packFolder);
|
new File(packFolder, "spawners"), new File(packFolder, "entities")));
|
||||||
runUnusedResourceGc(packFolder, packTextCorpus, removedUnusedFiles, warnings);
|
blockingErrors.addAll(validateCustomBiomeSpawns(
|
||||||
} catch (Throwable e) {
|
new File(packFolder, "biomes"), PackValidator::resolveEntitySpawnCategory));
|
||||||
IrisLogging.reportError("PackValidator GC pass failed for pack '" + packName + "'", e);
|
|
||||||
warnings.add("Unused-resource GC pass failed: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
runContentKeyValidation(packFolder, warnings);
|
runContentKeyValidation(packFolder, warnings);
|
||||||
|
|
||||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> validateUnsupportedStructureTransforms(File packFolder) {
|
||||||
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String folderName : STRUCTURE_HOST_FOLDERS) {
|
||||||
|
File resourceFolder = new File(packFolder, folderName);
|
||||||
|
if (!resourceFolder.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<File> resourceFiles = listJsonRecursive(resourceFolder);
|
||||||
|
resourceFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
String resourceType = structureHostType(folderName);
|
||||||
|
for (File resourceFile : resourceFiles) {
|
||||||
|
JSONObject resource;
|
||||||
|
try {
|
||||||
|
resource = new JSONObject(Files.readString(resourceFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray placements = resource.optJSONArray("structures");
|
||||||
|
if (placements == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String resourceKey = deriveKey(resourceFolder, resourceFile);
|
||||||
|
for (int placementIndex = 0; placementIndex < placements.length(); placementIndex++) {
|
||||||
|
JSONObject placement = placements.optJSONObject(placementIndex);
|
||||||
|
if (placement == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String field : UNSUPPORTED_STRUCTURE_TRANSFORM_FIELDS) {
|
||||||
|
if (placement.has(field)) {
|
||||||
|
blockingErrors.add(resourceType + " '" + resourceKey + "' structures[" + placementIndex
|
||||||
|
+ "] declares unsupported field '" + field
|
||||||
|
+ "'. Structure placement transforms are not supported; remove the field.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String structureHostType(String folderName) {
|
||||||
|
return switch (folderName) {
|
||||||
|
case "dimensions" -> "Dimension";
|
||||||
|
case "regions" -> "Region";
|
||||||
|
case "biomes" -> "Biome";
|
||||||
|
default -> "Resource";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> validateSpawnerEntityReferences(File spawnersFolder, File entitiesFolder) {
|
||||||
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
|
if (spawnersFolder == null || !spawnersFolder.isDirectory()) {
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path entityRoot = entitiesFolder.toPath().toAbsolutePath().normalize();
|
||||||
|
Set<Path> validEntityFiles = new HashSet<>();
|
||||||
|
Map<Path, String> invalidEntityFiles = new HashMap<>();
|
||||||
|
List<File> spawnerFiles = listJsonRecursive(spawnersFolder);
|
||||||
|
spawnerFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
for (File spawnerFile : spawnerFiles) {
|
||||||
|
String spawnerKey = deriveKey(spawnersFolder, spawnerFile);
|
||||||
|
JSONObject spawner;
|
||||||
|
try {
|
||||||
|
spawner = new JSONObject(Files.readString(spawnerFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' has invalid JSON: " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSpawnerSpawnEntries(spawnerKey, spawner, "spawns", entityRoot,
|
||||||
|
validEntityFiles, invalidEntityFiles, blockingErrors);
|
||||||
|
validateSpawnerSpawnEntries(spawnerKey, spawner, "initialSpawns", entityRoot,
|
||||||
|
validEntityFiles, invalidEntityFiles, blockingErrors);
|
||||||
|
}
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateSpawnerSpawnEntries(String spawnerKey,
|
||||||
|
JSONObject spawner,
|
||||||
|
String field,
|
||||||
|
Path entityRoot,
|
||||||
|
Set<Path> validEntityFiles,
|
||||||
|
Map<Path, String> invalidEntityFiles,
|
||||||
|
List<String> blockingErrors) {
|
||||||
|
if (!spawner.has(field)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONArray entries = spawner.optJSONArray(field);
|
||||||
|
if (entries == null) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " must be an array.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int index = 0; index < entries.length(); index++) {
|
||||||
|
JSONObject entry = entries.optJSONObject(index);
|
||||||
|
if (entry == null) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
|
||||||
|
+ " has a non-object entry at index " + index + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.has("entity") || entry.isNull("entity")) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
|
||||||
|
+ " has an entry without an entity reference at index " + index + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object rawEntity = entry.get("entity");
|
||||||
|
if (!(rawEntity instanceof String entityKey)) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
|
||||||
|
+ " entity reference at index " + index + " must be a string.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entityKey.isBlank()) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field
|
||||||
|
+ " has a blank entity reference at index " + index + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path entityFile;
|
||||||
|
try {
|
||||||
|
if (entityKey.indexOf('\\') >= 0) {
|
||||||
|
throw new IllegalArgumentException("backslash path separators are not portable");
|
||||||
|
}
|
||||||
|
entityFile = entityRoot.resolve(entityKey + ".json").normalize();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
|
||||||
|
+ " has invalid entity reference '" + entityKey + "': " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entityFile.startsWith(entityRoot)) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
|
||||||
|
+ " has unsafe entity reference '" + entityKey + "'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Files.isRegularFile(entityFile)) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
|
||||||
|
+ " references missing entity '" + entityKey + "'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String invalidJson = invalidEntityFiles.get(entityFile);
|
||||||
|
if (invalidJson != null) {
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
|
||||||
|
+ " references malformed entity '" + entityKey + "': " + invalidJson);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (validEntityFiles.contains(entityFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new JSONObject(Files.readString(entityFile, StandardCharsets.UTF_8));
|
||||||
|
validEntityFiles.add(entityFile);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||||
|
invalidEntityFiles.put(entityFile, message);
|
||||||
|
blockingErrors.add("Spawner '" + spawnerKey + "' " + field + " entry at index " + index
|
||||||
|
+ " references malformed entity '" + entityKey + "': " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> validateCustomBiomeSpawns(File biomesFolder, Function<String, SpawnCategoryResolution> categoryResolver) {
|
||||||
|
List<String> blockingErrors = new ArrayList<>();
|
||||||
|
if (biomesFolder == null || !biomesFolder.isDirectory()) {
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> biomeFiles = listJsonRecursive(biomesFolder);
|
||||||
|
biomeFiles.sort(Comparator.comparing(File::getPath));
|
||||||
|
for (File biomeFile : biomeFiles) {
|
||||||
|
String biomeKey = deriveKey(biomesFolder, biomeFile);
|
||||||
|
JSONObject biome;
|
||||||
|
try {
|
||||||
|
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' has invalid JSON: " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray derivatives = biome.optJSONArray("customDerivitives");
|
||||||
|
if (derivatives == null) {
|
||||||
|
if (biome.has("customDerivitives") && !biome.isNull("customDerivitives")) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' customDerivitives must be an array.");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int derivativeIndex = 0; derivativeIndex < derivatives.length(); derivativeIndex++) {
|
||||||
|
JSONObject derivative = derivatives.optJSONObject(derivativeIndex);
|
||||||
|
if (derivative == null) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' has a non-object custom derivative at index " + derivativeIndex + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validateCustomBiomeDerivativeTags(biomeKey, derivative, derivativeIndex, blockingErrors);
|
||||||
|
validateCustomBiomeDerivativeSpawns(
|
||||||
|
biomeKey, derivative, derivativeIndex, categoryResolver, blockingErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blockingErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateCustomBiomeDerivativeTags(String biomeKey,
|
||||||
|
JSONObject derivative,
|
||||||
|
int derivativeIndex,
|
||||||
|
List<String> blockingErrors) {
|
||||||
|
if (!derivative.has("tags") || derivative.isNull("tags")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
|
||||||
|
JSONArray tags = derivative.optJSONArray("tags");
|
||||||
|
if (tags == null) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' tags must be an array.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int tagIndex = 0; tagIndex < tags.length(); tagIndex++) {
|
||||||
|
Object rawTag = tags.opt(tagIndex);
|
||||||
|
if (!(rawTag instanceof String tag)) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' has a non-string tag at index " + tagIndex + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String normalized = tag.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (normalized.indexOf(':') < 0) {
|
||||||
|
normalized = "minecraft:" + normalized;
|
||||||
|
}
|
||||||
|
if (!isSafeResourceKey(normalized)) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' has invalid tag '" + tag + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSafeResourceKey(String key) {
|
||||||
|
if (!RESOURCE_KEY_PATTERN.matcher(key).matches()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int separator = key.indexOf(':');
|
||||||
|
String[] segments = key.substring(separator + 1).split("/");
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment.equals("..")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateCustomBiomeDerivativeSpawns(String biomeKey,
|
||||||
|
JSONObject derivative,
|
||||||
|
int derivativeIndex,
|
||||||
|
Function<String, SpawnCategoryResolution> categoryResolver,
|
||||||
|
List<String> blockingErrors) {
|
||||||
|
JSONArray spawns = derivative.optJSONArray("spawns");
|
||||||
|
if (spawns == null) {
|
||||||
|
if (derivative.has("spawns") && !derivative.isNull("spawns")) {
|
||||||
|
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawns must be an array.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String derivativeId = derivative.optString("id", "#" + derivativeIndex);
|
||||||
|
for (int spawnIndex = 0; spawnIndex < spawns.length(); spawnIndex++) {
|
||||||
|
JSONObject spawn = spawns.optJSONObject(spawnIndex);
|
||||||
|
if (spawn == null) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' has a non-object spawn at index " + spawnIndex + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = spawn.optString("type", "").trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (type.isEmpty()) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' has a spawn without an entity type at index " + spawnIndex + ".");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String typeKey = type.indexOf(':') >= 0 ? type : "minecraft:" + type;
|
||||||
|
SpawnCategoryResolution resolution;
|
||||||
|
try {
|
||||||
|
resolution = categoryResolver == null ? null : categoryResolver.apply(typeKey);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError("PackValidator failed to resolve spawn category for '" + typeKey + "'", e);
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawn category lookup failed for '" + typeKey + "': " + e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resolution != null && !resolution.entityKnown()) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawn references unknown entity type '" + typeKey + "'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String expectedGroup = resolution == null ? null : resolution.category();
|
||||||
|
String group = spawn.optString("group", "").trim();
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
if (expectedGroup != null && !expectedGroup.isBlank()
|
||||||
|
&& !IrisBiomeCustomSpawnType.MISC.name().equalsIgnoreCase(expectedGroup)) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawn '" + typeKey + "' must declare group '"
|
||||||
|
+ expectedGroup.toUpperCase(Locale.ROOT) + "'.");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiomeCustomSpawnType configuredGroup;
|
||||||
|
try {
|
||||||
|
configuredGroup = IrisBiomeCustomSpawnType.valueOf(group);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawn '" + typeKey + "' declares unknown group '" + group + "'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedGroup != null && !expectedGroup.isBlank()
|
||||||
|
&& !configuredGroup.name().equalsIgnoreCase(expectedGroup)) {
|
||||||
|
blockingErrors.add("Biome '" + biomeKey + "' custom derivative '" + derivativeId
|
||||||
|
+ "' spawn '" + typeKey + "' declares group '" + configuredGroup.name()
|
||||||
|
+ "' but the live entity registry requires '" + expectedGroup.toUpperCase(Locale.ROOT) + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SpawnCategoryResolution resolveEntitySpawnCategory(String typeKey) {
|
||||||
|
if (!IrisPlatforms.isBound()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlatformRegistries registries = IrisPlatforms.get().registries();
|
||||||
|
if (registries == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlatformEntityType entityType = registries.entity(typeKey);
|
||||||
|
return entityType == null
|
||||||
|
? SpawnCategoryResolution.unknown()
|
||||||
|
: SpawnCategoryResolution.known(entityType.spawnCategory());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runContentKeyValidation(File packFolder, List<String> warnings) {
|
private static void runContentKeyValidation(File packFolder, List<String> warnings) {
|
||||||
@@ -221,6 +549,16 @@ public final class PackValidator {
|
|||||||
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
|
private record ReferencedContentKeys(Set<String> blocks, Set<String> items, Set<String> entities) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record SpawnCategoryResolution(boolean entityKnown, String category) {
|
||||||
|
static SpawnCategoryResolution unknown() {
|
||||||
|
return new SpawnCategoryResolution(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SpawnCategoryResolution known(String category) {
|
||||||
|
return new SpawnCategoryResolution(true, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
||||||
File regionsFolder = new File(packFolder, "regions");
|
File regionsFolder = new File(packFolder, "regions");
|
||||||
File biomesFolder = new File(packFolder, "biomes");
|
File biomesFolder = new File(packFolder, "biomes");
|
||||||
@@ -299,24 +637,6 @@ public final class PackValidator {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String buildPackTextCorpus(File packFolder) {
|
|
||||||
StringBuilder sb = new StringBuilder(1 << 16);
|
|
||||||
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
|
|
||||||
stream.filter(Files::isRegularFile)
|
|
||||||
.filter(PackValidator::isScannableJsonPath)
|
|
||||||
.forEach(p -> {
|
|
||||||
try {
|
|
||||||
sb.append(Files.readString(p, StandardCharsets.UTF_8));
|
|
||||||
sb.append('\n');
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError("PackValidator failed to walk pack folder for corpus scan", e);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isScannableJsonPath(Path path) {
|
private static boolean isScannableJsonPath(Path path) {
|
||||||
String name = path.getFileName().toString();
|
String name = path.getFileName().toString();
|
||||||
if (!name.endsWith(".json")) {
|
if (!name.endsWith(".json")) {
|
||||||
@@ -350,66 +670,6 @@ public final class PackValidator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runUnusedResourceGc(File packFolder, String corpus, List<String> removedUnusedFiles, List<String> warnings) {
|
|
||||||
if (corpus == null || corpus.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
File trashRoot = new File(packFolder, TRASH_ROOT + File.separator + LocalDateTime.now().format(TRASH_STAMP));
|
|
||||||
Set<File> scheduledForTrash = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
for (String folderName : MANAGED_RESOURCE_FOLDERS) {
|
|
||||||
File resourceFolder = new File(packFolder, folderName);
|
|
||||||
if (!resourceFolder.isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<File> files = listJsonRecursive(resourceFolder);
|
|
||||||
for (File resourceFile : files) {
|
|
||||||
String key = deriveKey(resourceFolder, resourceFile);
|
|
||||||
if (key == null || key.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isReferenced(corpus, key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
scheduledForTrash.add(resourceFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduledForTrash.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (File file : scheduledForTrash) {
|
|
||||||
try {
|
|
||||||
Path src = file.toPath();
|
|
||||||
Path relative = packFolder.toPath().relativize(src);
|
|
||||||
Path dest = trashRoot.toPath().resolve(relative);
|
|
||||||
Files.createDirectories(dest.getParent());
|
|
||||||
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
removedUnusedFiles.add(relative.toString().replace(File.separatorChar, '/'));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError("PackValidator failed to move unused file " + file.getPath() + " to trash", e);
|
|
||||||
warnings.add("Failed to quarantine unused file " + file.getName() + ": " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isReferenced(String corpus, String key) {
|
|
||||||
String needleQuoted = "\"" + key + "\"";
|
|
||||||
if (corpus.contains(needleQuoted)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
int slash = key.indexOf('/');
|
|
||||||
if (slash > 0) {
|
|
||||||
String tail = key.substring(slash + 1);
|
|
||||||
if (!tail.isBlank() && corpus.contains("\"" + tail + "\"")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<File> listJsonRecursive(File root) {
|
private static List<File> listJsonRecursive(File root) {
|
||||||
List<File> out = new ArrayList<>();
|
List<File> out = new ArrayList<>();
|
||||||
try (Stream<Path> stream = Files.walk(root.toPath())) {
|
try (Stream<Path> stream = Files.walk(root.toPath())) {
|
||||||
@@ -435,49 +695,6 @@ public final class PackValidator {
|
|||||||
return dot <= 0 ? name : name.substring(0, dot);
|
return dot <= 0 ? name : name.substring(0, dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int restoreTrash(File packFolder) {
|
|
||||||
if (packFolder == null || !packFolder.isDirectory()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
File trashRoot = new File(packFolder, TRASH_ROOT);
|
|
||||||
if (!trashRoot.isDirectory()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
File[] dumps = trashRoot.listFiles(File::isDirectory);
|
|
||||||
if (dumps == null || dumps.length == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Arrays.sort(dumps, Comparator.comparing(File::getName));
|
|
||||||
File latestDump = dumps[dumps.length - 1];
|
|
||||||
int restored = 0;
|
|
||||||
try (Stream<Path> stream = Files.walk(latestDump.toPath())) {
|
|
||||||
List<Path> files = stream.filter(Files::isRegularFile).toList();
|
|
||||||
for (Path src : files) {
|
|
||||||
Path relative = latestDump.toPath().relativize(src);
|
|
||||||
Path dest = packFolder.toPath().resolve(relative);
|
|
||||||
Files.createDirectories(dest.getParent());
|
|
||||||
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
restored++;
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError("PackValidator failed to restore trash for pack " + packFolder.getName(), e);
|
|
||||||
}
|
|
||||||
deleteFolderQuiet(latestDump);
|
|
||||||
return restored;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void deleteFolderQuiet(File folder) {
|
|
||||||
if (folder == null || !folder.exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (Stream<Path> stream = Files.walk(folder.toPath())) {
|
|
||||||
stream.sorted(Comparator.reverseOrder())
|
|
||||||
.map(Path::toFile)
|
|
||||||
.forEach(File::delete);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
|
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
|
||||||
Set<String> keys = new HashSet<>();
|
Set<String> keys = new HashSet<>();
|
||||||
if (corpus == null) {
|
if (corpus == null) {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import art.arcane.iris.core.IrisSettings;
|
|||||||
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.iris.util.common.format.C;
|
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.math.M;
|
import art.arcane.volmlib.util.math.M;
|
||||||
@@ -217,16 +216,21 @@ public class IrisPregenerator {
|
|||||||
} finally {
|
} finally {
|
||||||
shutdown();
|
shutdown();
|
||||||
}
|
}
|
||||||
if (completed) {
|
if (completed && allVisitsComplete()) {
|
||||||
logSuccessfulCompletion(p);
|
logSuccessfulCompletion(p);
|
||||||
}
|
|
||||||
if (benchmarking == null) {
|
|
||||||
IrisLogging.info(C.IRIS + "Pregen stopped.");
|
|
||||||
} else {
|
} else {
|
||||||
|
logIncompleteCompletion(p);
|
||||||
|
}
|
||||||
|
if (benchmarking != null) {
|
||||||
benchmarking.finishedBenchmark(chunksPerSecondHistory);
|
benchmarking.finishedBenchmark(chunksPerSecondHistory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean allVisitsComplete() {
|
||||||
|
long total = totalChunks.get();
|
||||||
|
return total > 0 && generated.get() + failed.get() >= total;
|
||||||
|
}
|
||||||
|
|
||||||
private void logSuccessfulCompletion(PrecisionStopwatch stopwatch) {
|
private void logSuccessfulCompletion(PrecisionStopwatch stopwatch) {
|
||||||
long failedCount = failed.get();
|
long failedCount = failed.get();
|
||||||
if (failedCount > 0) {
|
if (failedCount > 0) {
|
||||||
@@ -238,6 +242,19 @@ public class IrisPregenerator {
|
|||||||
+ " duration=" + Form.duration((long) stopwatch.getMilliseconds()));
|
+ " duration=" + Form.duration((long) stopwatch.getMilliseconds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void logIncompleteCompletion(PrecisionStopwatch stopwatch) {
|
||||||
|
long generatedCount = generated.get();
|
||||||
|
long failedCount = failed.get();
|
||||||
|
long total = totalChunks.get();
|
||||||
|
long remaining = Math.max(0L, total - generatedCount - failedCount);
|
||||||
|
String status = shutdown.get() ? "Pregen cancelled" : "Pregen stopped before completion";
|
||||||
|
IrisLogging.info(status + ": generated=" + Form.f(generatedCount)
|
||||||
|
+ " total=" + Form.f(total)
|
||||||
|
+ " failed=" + Form.f(failedCount)
|
||||||
|
+ " remaining=" + Form.f(remaining)
|
||||||
|
+ " duration=" + Form.duration((long) stopwatch.getMilliseconds()));
|
||||||
|
}
|
||||||
|
|
||||||
private void checkRegions() {
|
private void checkRegions() {
|
||||||
task.iterateRegions(this::checkRegion);
|
task.iterateRegions(this::checkRegion);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ public class IrisProject {
|
|||||||
biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm))));
|
biomes.forEach((i) -> i.getGenerators().forEach((j) -> generators.add(j.getCachedGenerator(() -> dm))));
|
||||||
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
|
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> loot.add(dm.getLootLoader().load(i))));
|
||||||
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
|
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> spawners.add(dm.getSpawnerLoader().load(sp))));
|
||||||
spawners.forEach((i) -> i.getSpawns().forEach((j) -> entities.add(dm.getEntityLoader().load(j.getEntity()))));
|
collectSpawnerEntityKeys(spawners).forEach((i) -> entities.add(dm.getEntityLoader().load(i)));
|
||||||
KMap<String, String> renameObjects = new KMap<>();
|
KMap<String, String> renameObjects = new KMap<>();
|
||||||
String a;
|
String a;
|
||||||
StringBuilder b = new StringBuilder();
|
StringBuilder b = new StringBuilder();
|
||||||
@@ -769,6 +769,15 @@ public class IrisProject {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static KSet<String> collectSpawnerEntityKeys(KSet<IrisSpawner> spawners) {
|
||||||
|
KSet<String> entityKeys = new KSet<>();
|
||||||
|
for (IrisSpawner spawner : spawners) {
|
||||||
|
spawner.getSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
|
||||||
|
spawner.getInitialSpawns().forEach((spawn) -> entityKeys.add(spawn.getEntity()));
|
||||||
|
}
|
||||||
|
return entityKeys;
|
||||||
|
}
|
||||||
|
|
||||||
public void compile(VolmitSender sender) {
|
public void compile(VolmitSender sender) {
|
||||||
IrisData data = IrisData.get(getPath());
|
IrisData data = IrisData.get(getPath());
|
||||||
KList<Job> jobs = new KList<>();
|
KList<Job> jobs = new KList<>();
|
||||||
|
|||||||
@@ -130,8 +130,17 @@ public class StudioSVC implements IrisService {
|
|||||||
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) {
|
public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
|
||||||
return installInto(sender, type, new File(folder, "iris/pack"));
|
File target = new File(folder, "iris/pack");
|
||||||
|
File source = dimension.getLoader().getDataFolder();
|
||||||
|
sender.sendMessage("Installing Package: " + source.getName() + ":" + dimension.getLoadKey());
|
||||||
|
try {
|
||||||
|
FileUtils.copyDirectory(source, target);
|
||||||
|
} catch (IOException e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return IrisData.get(target).getDimensionLoader().load(dimension.getLoadKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension installInto(VolmitSender sender, String type, File folder) {
|
public IrisDimension installInto(VolmitSender sender, String type, File folder) {
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ public class IrisCreator {
|
|||||||
|
|
||||||
reportStudioProgress(0.16D, "prepare_world_pack");
|
reportStudioProgress(0.16D, "prepare_world_pack");
|
||||||
if (!studio() || benchmark) {
|
if (!studio() || benchmark) {
|
||||||
IrisServices.get(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
|
d = IrisServices.get(StudioSVC.class).installIntoWorld(sender, d, new File(Bukkit.getWorldContainer(), name()));
|
||||||
|
if (d == null) {
|
||||||
|
throw new IrisException("Failed to install dimension pack for " + dimension());
|
||||||
|
}
|
||||||
|
dimension = d.getLoadKey();
|
||||||
}
|
}
|
||||||
if (studio()) {
|
if (studio()) {
|
||||||
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
|
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
|
||||||
@@ -178,7 +182,7 @@ public class IrisCreator {
|
|||||||
AtomicDouble pp = new AtomicDouble(0);
|
AtomicDouble pp = new AtomicDouble(0);
|
||||||
AtomicBoolean done = new AtomicBoolean(false);
|
AtomicBoolean done = new AtomicBoolean(false);
|
||||||
WorldCreator wc = new IrisWorldCreator()
|
WorldCreator wc = new IrisWorldCreator()
|
||||||
.dimension(dimension)
|
.dimension(d)
|
||||||
.name(name)
|
.name(name)
|
||||||
.seed(seed)
|
.seed(seed)
|
||||||
.studio(studio)
|
.studio(studio)
|
||||||
|
|||||||
@@ -81,23 +81,23 @@ public class IrisToolbelt {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String requested = dimension.trim();
|
PackReference reference = parsePackReference(dimension);
|
||||||
if (requested.isEmpty()) {
|
if (reference == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
File packsFolder = IrisPlatforms.get().dataFolder("packs");
|
File packsFolder = IrisPlatforms.get().dataFolder("packs");
|
||||||
File pack = new File(packsFolder, requested);
|
File pack = new File(packsFolder, reference.pack());
|
||||||
if (!pack.exists()) {
|
if (!pack.exists()) {
|
||||||
File found = findCaseInsensitivePack(packsFolder, requested);
|
File found = findCaseInsensitivePack(packsFolder, reference.pack());
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
pack = found;
|
pack = found;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pack.exists()) {
|
if (!pack.exists()) {
|
||||||
IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), BukkitPlatform.volmitPlugin().getTag()), requested, false);
|
IrisServices.get(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), BukkitPlatform.volmitPlugin().getTag()), reference.pack(), false);
|
||||||
File found = findCaseInsensitivePack(packsFolder, requested);
|
File found = findCaseInsensitivePack(packsFolder, reference.pack());
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
pack = found;
|
pack = found;
|
||||||
}
|
}
|
||||||
@@ -108,13 +108,16 @@ public class IrisToolbelt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisData data = IrisData.get(pack);
|
IrisData data = IrisData.get(pack);
|
||||||
IrisDimension resolved = data.getDimensionLoader().load(requested, false);
|
IrisDimension resolved = data.getDimensionLoader().load(reference.dimension(), false);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
if (reference.explicitDimension()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String packName = pack.getName();
|
String packName = pack.getName();
|
||||||
if (!packName.equals(requested)) {
|
if (!packName.equals(reference.pack())) {
|
||||||
resolved = data.getDimensionLoader().load(packName, false);
|
resolved = data.getDimensionLoader().load(packName, false);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
return resolved;
|
return resolved;
|
||||||
@@ -122,7 +125,7 @@ public class IrisToolbelt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
||||||
if (!key.equalsIgnoreCase(requested) && !key.equalsIgnoreCase(packName)) {
|
if (!key.equalsIgnoreCase(reference.pack()) && !key.equalsIgnoreCase(packName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +138,26 @@ public class IrisToolbelt {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PackReference parsePackReference(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String requested = value.trim();
|
||||||
|
if (requested.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int separator = requested.indexOf(':');
|
||||||
|
if (separator < 0) {
|
||||||
|
return new PackReference(requested, requested, false);
|
||||||
|
}
|
||||||
|
String pack = requested.substring(0, separator).trim();
|
||||||
|
String dimension = requested.substring(separator + 1).trim();
|
||||||
|
if (pack.isEmpty() || dimension.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new PackReference(pack, dimension, true);
|
||||||
|
}
|
||||||
|
|
||||||
private static File findCaseInsensitivePack(File packsFolder, String requested) {
|
private static File findCaseInsensitivePack(File packsFolder, String requested) {
|
||||||
File[] children = packsFolder.listFiles();
|
File[] children = packsFolder.listFiles();
|
||||||
if (children == null) {
|
if (children == null) {
|
||||||
@@ -503,4 +526,7 @@ public class IrisToolbelt {
|
|||||||
public static boolean removeWorld(World world) throws IOException {
|
public static boolean removeWorld(World world) throws IOException {
|
||||||
return IrisCreator.removeFromBukkitYml(world.getName());
|
return IrisCreator.removeFromBukkitYml(world.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record PackReference(String pack, String dimension, boolean explicitDimension) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class IrisWorldCreator {
|
|||||||
private String name;
|
private String name;
|
||||||
private boolean studio = false;
|
private boolean studio = false;
|
||||||
private String dimensionName = null;
|
private String dimensionName = null;
|
||||||
|
private IrisDimension dimension;
|
||||||
private long seed = 1337;
|
private long seed = 1337;
|
||||||
|
|
||||||
public IrisWorldCreator() {
|
public IrisWorldCreator() {
|
||||||
@@ -41,6 +42,13 @@ public class IrisWorldCreator {
|
|||||||
|
|
||||||
public IrisWorldCreator dimension(String loadKey) {
|
public IrisWorldCreator dimension(String loadKey) {
|
||||||
this.dimensionName = loadKey;
|
this.dimensionName = loadKey;
|
||||||
|
this.dimension = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisWorldCreator dimension(IrisDimension dimension) {
|
||||||
|
this.dimension = dimension;
|
||||||
|
this.dimensionName = dimension.getLoadKey();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +73,7 @@ public class IrisWorldCreator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public WorldCreator create() {
|
public WorldCreator create() {
|
||||||
IrisDimension dim = IrisData.loadAnyDimension(dimensionName, null);
|
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||||
|
|
||||||
IrisWorld w = IrisWorld.builder()
|
IrisWorld w = IrisWorld.builder()
|
||||||
.name(name)
|
.name(name)
|
||||||
@@ -87,7 +95,7 @@ public class IrisWorldCreator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private World.Environment findEnvironment() {
|
private World.Environment findEnvironment() {
|
||||||
IrisDimension dim = IrisData.loadAnyDimension(dimensionName, null);
|
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||||
if (dim == null || dim.getEnvironment() == null) {
|
if (dim == null || dim.getEnvironment() == null) {
|
||||||
return World.Environment.NORMAL;
|
return World.Environment.NORMAL;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
|
|||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -56,13 +58,14 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = "data")
|
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache"})
|
||||||
@ToString(exclude = "data")
|
@ToString(exclude = {"data", "gridBoundsCache"})
|
||||||
public class IrisComplex implements DataProvider {
|
public class IrisComplex implements DataProvider {
|
||||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||||
private static final int HEIGHT_BOUNDS_GRID = 4;
|
private static final int HEIGHT_BOUNDS_GRID = 4;
|
||||||
private static final ThreadLocal<GridBoundsCache> GRID_BOUNDS_CACHE = ThreadLocal.withInitial(GridBoundsCache::new);
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
||||||
private RNG rng;
|
private RNG rng;
|
||||||
private double fluidHeight;
|
private double fluidHeight;
|
||||||
private IrisData data;
|
private IrisData data;
|
||||||
@@ -362,7 +365,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
double fx = (x - gx) / grid;
|
double fx = (x - gx) / grid;
|
||||||
double fz = (z - gz) / grid;
|
double fz = (z - gz) / grid;
|
||||||
|
|
||||||
GridBoundsCache cache = GRID_BOUNDS_CACHE.get();
|
GridBoundsCache cache = gridBoundsCache.get();
|
||||||
long b00 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
|
long b00 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
|
||||||
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
|
long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz);
|
||||||
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
|
long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid);
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
|||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -78,6 +80,8 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -86,6 +90,8 @@ import java.util.stream.Stream;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Data
|
@Data
|
||||||
public class IrisWorldManager extends EngineAssignedWorldManager {
|
public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||||
|
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
|
||||||
|
|
||||||
private final Looper looper;
|
private final Looper looper;
|
||||||
private final int id;
|
private final int id;
|
||||||
private final KList<Runnable> updateQueue = new KList<>();
|
private final KList<Runnable> updateQueue = new KList<>();
|
||||||
@@ -100,11 +106,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<Long> markerScanQueue = ConcurrentHashMap.newKeySet();
|
private final Set<Long> markerScanQueue = ConcurrentHashMap.newKeySet();
|
||||||
|
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
|
||||||
|
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
|
||||||
private int entityCount = 0;
|
private int entityCount = 0;
|
||||||
private int actuallySpawned = 0;
|
private int actuallySpawned = 0;
|
||||||
private int cooldown = 0;
|
private int cooldown = 0;
|
||||||
private List<Entity> precount = new KList<>();
|
private int forcedChunkUpdateCursor = 0;
|
||||||
|
private volatile boolean playersPresent = false;
|
||||||
private KSet<Position2> injectBiomes = new KSet<>();
|
private KSet<Position2> injectBiomes = new KSet<>();
|
||||||
|
private volatile int loadedChunkCount = 0;
|
||||||
|
|
||||||
public IrisWorldManager() {
|
public IrisWorldManager() {
|
||||||
super(null);
|
super(null);
|
||||||
@@ -126,7 +137,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
cl = new ChronoLatch(3000);
|
cl = new ChronoLatch(3000);
|
||||||
clw = new ChronoLatch(1000, true);
|
clw = new ChronoLatch(1000, true);
|
||||||
cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||||
var thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name());
|
Thread thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name());
|
||||||
thread.setPriority(Thread.MIN_PRIORITY);
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
return thread;
|
return thread;
|
||||||
});
|
});
|
||||||
@@ -139,18 +150,18 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!getEngine().getWorld().hasRealWorld() && clw.flip()) {
|
if (!getEngine().getWorld().hasRealWorld() && clw.flip()) {
|
||||||
getEngine().getWorld().tryGetRealWorld();
|
J.runGlobal(() -> getEngine().getWorld().tryGetRealWorld());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getEngine().getWorld().hasRealWorld()) {
|
if (getEngine().getWorld().hasRealWorld()) {
|
||||||
if (getEngine().getWorld().getPlayers().isEmpty()) {
|
|
||||||
return 5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunkUpdater.flip()) {
|
if (chunkUpdater.flip()) {
|
||||||
updateChunks();
|
updateChunks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!playersPresent) {
|
||||||
|
return 5000;
|
||||||
|
}
|
||||||
|
|
||||||
if (chunkDiscovery.flip()) {
|
if (chunkDiscovery.flip()) {
|
||||||
discoverChunks();
|
discoverChunks();
|
||||||
}
|
}
|
||||||
@@ -163,19 +174,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return 3000;
|
return 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (precount != null) {
|
|
||||||
entityCount = 0;
|
|
||||||
for (Entity i : precount) {
|
|
||||||
if (i instanceof LivingEntity) {
|
|
||||||
if (!i.isDead()) {
|
|
||||||
entityCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
precount = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onAsyncTick();
|
onAsyncTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,23 +200,46 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Player player : getEngine().getWorld().getPlayers()) {
|
if (!chunkDiscoveryScanScheduled.compareAndSet(false, true)) {
|
||||||
if (player == null || !player.isOnline()) {
|
return;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
J.runEntity(player, () -> {
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
|
try {
|
||||||
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
|
if (getEngine().isClosed() || !world.equals(getEngine().getWorld().realWorld())) {
|
||||||
int radius = 1;
|
return;
|
||||||
for (int x = -radius; x <= radius; x++) {
|
|
||||||
for (int z = -radius; z <= radius; z++) {
|
|
||||||
int chunkX = centerX + x;
|
|
||||||
int chunkZ = centerZ + z;
|
|
||||||
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
for (Player player : world.getPlayers()) {
|
||||||
|
if (player == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
J.runEntity(player, () -> {
|
||||||
|
if (!player.isOnline() || !world.equals(player.getWorld())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
|
||||||
|
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
|
||||||
|
int radius = 1;
|
||||||
|
for (int x = -radius; x <= radius; x++) {
|
||||||
|
for (int z = -radius; z <= radius; z++) {
|
||||||
|
int chunkX = centerX + x;
|
||||||
|
int chunkZ = centerZ + z;
|
||||||
|
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
} finally {
|
||||||
|
chunkDiscoveryScanScheduled.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
chunkDiscoveryScanScheduled.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,24 +282,98 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Player player : getEngine().getWorld().getPlayers()) {
|
if (!chunkUpdateScanScheduled.compareAndSet(false, true)) {
|
||||||
if (player == null || !player.isOnline()) {
|
return;
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
boolean scheduled = J.runGlobal(() -> updateChunksOnGlobal(world));
|
||||||
|
if (!scheduled) {
|
||||||
|
chunkUpdateScanScheduled.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChunksOnGlobal(World world) {
|
||||||
|
try {
|
||||||
|
if (getEngine().isClosed() || !world.equals(getEngine().getWorld().realWorld())) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.runEntity(player, () -> {
|
List<Player> players = new ArrayList<>(world.getPlayers());
|
||||||
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
|
playersPresent = !players.isEmpty();
|
||||||
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
|
loadedChunkCount = world.getLoadedChunks().length;
|
||||||
int radius = 1;
|
for (Player player : players) {
|
||||||
|
if (player == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (int x = -radius; x <= radius; x++) {
|
J.runEntity(player, () -> schedulePlayerChunkUpdates(world, player));
|
||||||
for (int z = -radius; z <= radius; z++) {
|
}
|
||||||
int targetX = centerX + x;
|
|
||||||
int targetZ = centerZ + z;
|
scheduleForcedChunkUpdates(world);
|
||||||
J.runRegion(world, targetX, targetZ, () -> updateChunkRegion(world, targetX, targetZ));
|
} catch (Throwable e) {
|
||||||
}
|
IrisLogging.reportError(e);
|
||||||
|
} finally {
|
||||||
|
chunkUpdateScanScheduled.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void schedulePlayerChunkUpdates(World world, Player player) {
|
||||||
|
if (!player.isOnline() || !world.equals(player.getWorld())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int centerX = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockX());
|
||||||
|
int centerZ = PowerOfTwoCoordinates.blockToChunkFloor(player.getLocation().getBlockZ());
|
||||||
|
int radius = 1;
|
||||||
|
for (int x = -radius; x <= radius; x++) {
|
||||||
|
for (int z = -radius; z <= radius; z++) {
|
||||||
|
scheduleChunkUpdate(world, centerX + x, centerZ + z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleForcedChunkUpdates(World world) {
|
||||||
|
List<Position2> forcedChunks = new ArrayList<>();
|
||||||
|
for (Chunk chunk : world.getForceLoadedChunks()) {
|
||||||
|
forcedChunks.add(new Position2(chunk.getX(), chunk.getZ()));
|
||||||
|
}
|
||||||
|
forcedChunks.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ));
|
||||||
|
|
||||||
|
int forcedChunkCount = forcedChunks.size();
|
||||||
|
if (forcedChunkCount == 0) {
|
||||||
|
forcedChunkUpdateCursor = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int updateCount = Math.min(forcedChunkCount, MAX_FORCED_CHUNK_UPDATES);
|
||||||
|
int start = Math.floorMod(forcedChunkUpdateCursor, forcedChunkCount);
|
||||||
|
for (int i = 0; i < updateCount; i++) {
|
||||||
|
Position2 chunk = forcedChunks.get((start + i) % forcedChunkCount);
|
||||||
|
scheduleChunkUpdate(world, chunk.getX(), chunk.getZ());
|
||||||
|
}
|
||||||
|
forcedChunkUpdateCursor = (start + updateCount) % forcedChunkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleChunkUpdate(World world, int chunkX, int chunkZ) {
|
||||||
|
long key = Cache.key(chunkX, chunkZ);
|
||||||
|
if (!chunkUpdateQueue.add(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
updateChunkRegion(world, chunkX, chunkZ);
|
||||||
|
} finally {
|
||||||
|
chunkUpdateQueue.remove(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
chunkUpdateQueue.remove(key);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
chunkUpdateQueue.remove(key);
|
||||||
|
IrisLogging.reportError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +502,36 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cl.flip()) {
|
||||||
|
try {
|
||||||
|
World realWorld = getEngine().getWorld().realWorld();
|
||||||
|
if (realWorld == null) {
|
||||||
|
entityCount = 0;
|
||||||
|
} else if (J.isFolia()) {
|
||||||
|
entityCount = getFoliaLivingEntityCount(realWorld);
|
||||||
|
} else {
|
||||||
|
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||||
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
|
try {
|
||||||
|
int count = 0;
|
||||||
|
for (Entity entity : realWorld.getEntities()) {
|
||||||
|
if (entity instanceof LivingEntity && !entity.isDead()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
future.complete(count);
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
future.completeExceptionally(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
entityCount = scheduled ? future.get(2, TimeUnit.SECONDS) : 0;
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double epx = getEntitySaturation();
|
double epx = getEntitySaturation();
|
||||||
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
||||||
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
|
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
|
||||||
@@ -414,43 +539,20 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cl.flip()) {
|
|
||||||
try {
|
|
||||||
World realWorld = getEngine().getWorld().realWorld();
|
|
||||||
if (realWorld == null) {
|
|
||||||
precount = new KList<>();
|
|
||||||
} else if (J.isFolia()) {
|
|
||||||
precount = getFoliaEntitySnapshot(realWorld);
|
|
||||||
} else {
|
|
||||||
CompletableFuture<List<Entity>> future = new CompletableFuture<>();
|
|
||||||
J.s(() -> {
|
|
||||||
try {
|
|
||||||
future.complete(realWorld.getEntities());
|
|
||||||
} catch (Throwable ex) {
|
|
||||||
future.completeExceptionally(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
precount = future.get(2, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int spawnBuffer = RNG.r.i(2, 12);
|
int spawnBuffer = RNG.r.i(2, 12);
|
||||||
World world = getEngine().getWorld().realWorld();
|
World world = getEngine().getWorld().realWorld();
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk[] cc = getLoadedChunksSnapshot(world);
|
Position2[] cc = getLoadedChunkPositionsSnapshot(world);
|
||||||
while (spawnBuffer-- > 0) {
|
while (spawnBuffer-- > 0) {
|
||||||
if (cc.length == 0) {
|
if (cc.length == 0) {
|
||||||
IrisLogging.debug("Can't spawn. No chunks!");
|
IrisLogging.debug("Can't spawn. No chunks!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk c = cc[RNG.r.nextInt(cc.length)];
|
Position2 c = cc[RNG.r.nextInt(cc.length)];
|
||||||
spawnChunkSafely(world, c.getX(), c.getZ(), false);
|
spawnChunkSafely(world, c.getX(), c.getZ(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,48 +577,76 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return job.targetsWorldName(world.getName());
|
return job.targetsWorldName(world.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Chunk[] getLoadedChunksSnapshot(World world) {
|
private Position2[] getLoadedChunkPositionsSnapshot(World world) {
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
return new Chunk[0];
|
return new Position2[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletableFuture<Chunk[]> future = new CompletableFuture<>();
|
CompletableFuture<Position2[]> future = new CompletableFuture<>();
|
||||||
J.s(() -> {
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
try {
|
try {
|
||||||
future.complete(world.getLoadedChunks());
|
Chunk[] chunks = world.getLoadedChunks();
|
||||||
|
Position2[] positions = new Position2[chunks.length];
|
||||||
|
for (int i = 0; i < chunks.length; i++) {
|
||||||
|
positions[i] = new Position2(chunks[i].getX(), chunks[i].getZ());
|
||||||
|
}
|
||||||
|
loadedChunkCount = positions.length;
|
||||||
|
future.complete(positions);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
future.completeExceptionally(e);
|
future.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
return new Position2[0];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return future.get(2, TimeUnit.SECONDS);
|
return future.get(2, TimeUnit.SECONDS);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
return new Chunk[0];
|
return new Position2[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Entity> getFoliaEntitySnapshot(World world) {
|
private int getFoliaLivingEntityCount(World world) {
|
||||||
Map<String, Entity> snapshot = new ConcurrentHashMap<>();
|
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
|
||||||
List<Player> players = getEngine().getWorld().getPlayers();
|
boolean scheduled = J.runGlobal(() -> {
|
||||||
if (players == null || players.isEmpty()) {
|
try {
|
||||||
return new KList<>();
|
playerFuture.complete(new ArrayList<>(world.getPlayers()));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
playerFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Player> players;
|
||||||
|
try {
|
||||||
|
players = playerFuture.get(2, TimeUnit.SECONDS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Entity> candidates = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
CountDownLatch latch = new CountDownLatch(players.size());
|
CountDownLatch latch = new CountDownLatch(players.size());
|
||||||
for (Player player : players) {
|
for (Player player : players) {
|
||||||
if (player == null || !player.isOnline() || !world.equals(player.getWorld())) {
|
if (player == null) {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!J.runEntity(player, () -> {
|
if (!J.runEntity(player, () -> {
|
||||||
try {
|
try {
|
||||||
snapshot.put(player.getUniqueId().toString(), player);
|
if (!player.isOnline() || !world.equals(player.getWorld())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
candidates.put(player.getUniqueId().toString(), player);
|
||||||
for (Entity nearby : player.getNearbyEntities(64, 64, 64)) {
|
for (Entity nearby : player.getNearbyEntities(64, 64, 64)) {
|
||||||
if (nearby != null && world.equals(nearby.getWorld())) {
|
if (nearby != null) {
|
||||||
snapshot.put(nearby.getUniqueId().toString(), nearby);
|
candidates.put(nearby.getUniqueId().toString(), nearby);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -533,9 +663,28 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
KList<Entity> entities = new KList<>();
|
AtomicInteger count = new AtomicInteger();
|
||||||
entities.addAll(snapshot.values());
|
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
|
||||||
return entities;
|
for (Entity entity : candidates.values()) {
|
||||||
|
if (!J.runEntity(entity, () -> {
|
||||||
|
try {
|
||||||
|
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
|
||||||
|
count.incrementAndGet();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
entityLatch.countDown();
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
entityLatch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
entityLatch.await(2, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
return count.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
|
private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
|
||||||
@@ -1039,7 +1188,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getChunkCount() {
|
public int getChunkCount() {
|
||||||
return getEngine().getWorld().realWorld().getLoadedChunks().length;
|
return loadedChunkCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1048,7 +1197,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (double) entityCount / (getEngine().getWorld().realWorld().getLoadedChunks().length + 1) * 1.28;
|
return (double) entityCount / (loadedChunkCount + 1) * 1.28;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
PlatformBiome biome;
|
PlatformBiome biome;
|
||||||
|
|
||||||
if (ib.isCustom()) {
|
if (ib.isCustom()) {
|
||||||
IrisBiomeCustom custom = ib.getCustomBiome(rng, x, 0, z);
|
IrisBiomeCustom custom = ib.getCustomBiome(rng, getEngine(), x + xf, 0, z + zf);
|
||||||
String key = getDimension().getLoadKey() + ":" + custom.getId();
|
String key = getDimension().getLoadKey() + ":" + custom.getId();
|
||||||
biome = IrisPlatforms.get().registries().biome(key);
|
biome = IrisPlatforms.get().registries().biome(key);
|
||||||
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(key));
|
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(key));
|
||||||
} else {
|
} else {
|
||||||
String skyKey = ib.getSkyBiomeKey(rng, x, 0, z);
|
String skyKey = ib.getSkyBiomeKey(rng, getEngine(), x + xf, 0, z + zf);
|
||||||
biome = IrisPlatforms.get().registries().biome(skyKey);
|
biome = IrisPlatforms.get().registries().biome(skyKey);
|
||||||
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(skyKey));
|
matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(skyKey));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
|
|||||||
private static final Predicate<PlatformBlockState> PREDICATE_SOLID = (s) -> s != null && !B.isAirOrFluid(s);
|
private static final Predicate<PlatformBlockState> PREDICATE_SOLID = (s) -> s != null && !B.isAirOrFluid(s);
|
||||||
private final RNG rng;
|
private final RNG rng;
|
||||||
@Getter
|
@Getter
|
||||||
private final EngineDecorator surfaceDecorator;
|
private final IrisSurfaceDecorator surfaceDecorator;
|
||||||
@Getter
|
@Getter
|
||||||
private final EngineDecorator ceilingDecorator;
|
private final IrisCeilingDecorator ceilingDecorator;
|
||||||
@Getter
|
@Getter
|
||||||
private final EngineDecorator seaSurfaceDecorator;
|
private final EngineDecorator seaSurfaceDecorator;
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -41,7 +41,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
|
|||||||
@Override
|
@Override
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||||
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
|
decorate(x, z, realX, realX1, realX_1, realZ, realZ1, realZ_1, data, biome, biome.getInferredType(), height, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BlockCoordinates
|
||||||
|
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||||
|
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||||
|
boolean caveSkipFluid = IrisSurfaceDecorator.skipsFluid(inferredType);
|
||||||
RNG rng = getRNG(realX, realZ);
|
RNG rng = getRNG(realX, realZ);
|
||||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,19 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
@Override
|
@Override
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||||
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
|
||||||
|
decorate(x, z, realX, realX1, realX_1, realZ, realZ1, realZ_1, data, biome, biome.getInferredType(), height, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BlockCoordinates
|
||||||
|
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
||||||
|
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
|
||||||
int fluidHeight = getDimension().getFluidHeight();
|
int fluidHeight = getDimension().getFluidHeight();
|
||||||
if (biome.getInferredType().equals(InferredType.SHORE) && height < fluidHeight) {
|
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean underwater = height < fluidHeight && biome.getInferredType() != InferredType.CAVE;
|
boolean underwater = isUnderwater(inferredType, height, fluidHeight);
|
||||||
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
|
boolean caveSkipFluid = skipsFluid(inferredType);
|
||||||
RNG rng = getRNG(realX, realZ);
|
RNG rng = getRNG(realX, realZ);
|
||||||
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
|
||||||
|
|
||||||
@@ -79,4 +85,12 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
|
||||||
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean isUnderwater(InferredType inferredType, int height, int fluidHeight) {
|
||||||
|
return height < fluidHeight && inferredType != InferredType.CAVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean skipsFluid(InferredType inferredType) {
|
||||||
|
return inferredType == InferredType.CAVE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ import art.arcane.iris.engine.object.IrisEngineData;
|
|||||||
import art.arcane.iris.engine.object.IrisLootMode;
|
import art.arcane.iris.engine.object.IrisLootMode;
|
||||||
import art.arcane.iris.engine.object.IrisLootReference;
|
import art.arcane.iris.engine.object.IrisLootReference;
|
||||||
import art.arcane.iris.engine.object.IrisLootTable;
|
import art.arcane.iris.engine.object.IrisLootTable;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisPosition;
|
import art.arcane.iris.engine.object.IrisPosition;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformBiome;
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
@@ -577,15 +579,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] v = objectAt.split("\\Q@\\E");
|
StructurePlacementMarker.Decoded marker = StructurePlacementMarker.decode(objectAt);
|
||||||
String object = v[0];
|
if (marker == null) {
|
||||||
if (object.isEmpty() || object.equals("null")) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
String object = marker.objectKey();
|
||||||
if (object.startsWith("procedural/")) {
|
if (object.startsWith("procedural/")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int id = Integer.parseInt(v[1]);
|
int id = marker.placementId();
|
||||||
|
|
||||||
|
if (marker.structureAware()) {
|
||||||
|
IrisObject placedObject = getData().getObjectLoader().load(object);
|
||||||
|
IrisStructure structure = IrisData.loadAnyStructure(marker.structureKey(), getData());
|
||||||
|
IrisObjectPlacement placement = placedObject == null || structure == null
|
||||||
|
? null
|
||||||
|
: structure.createLootPlacement(object);
|
||||||
|
return new PlacedObject(placement, placedObject, id, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
IrisRegion region = getRegion(x, z);
|
IrisRegion region = getRegion(x, z);
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class StructurePlacementMarker {
|
||||||
|
private static final String FAMILY_PREFIX = "@iris-structure:";
|
||||||
|
private static final String VERSION_PREFIX = FAMILY_PREFIX + "v1:";
|
||||||
|
|
||||||
|
private StructurePlacementMarker() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encodeStructure(String objectKey, int placementId, String structureKey) {
|
||||||
|
String normalizedObjectKey = requireKey(objectKey, "objectKey");
|
||||||
|
String normalizedStructureKey = requireKey(structureKey, "structureKey");
|
||||||
|
return VERSION_PREFIX
|
||||||
|
+ encodeKey(normalizedObjectKey)
|
||||||
|
+ ":" + placementId
|
||||||
|
+ ":" + encodeKey(normalizedStructureKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Decoded decode(String marker) {
|
||||||
|
if (marker == null || marker.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (marker.startsWith(FAMILY_PREFIX)) {
|
||||||
|
return decodeStructure(marker);
|
||||||
|
}
|
||||||
|
return decodeLegacy(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Decoded decodeStructure(String marker) {
|
||||||
|
if (!marker.startsWith(VERSION_PREFIX)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] fields = marker.split(":", -1);
|
||||||
|
if (fields.length != 5 || !fields[0].equals("@iris-structure") || !fields[1].equals("v1")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String objectKey = decodeCanonicalKey(fields[2]);
|
||||||
|
int placementId = Integer.parseInt(fields[3]);
|
||||||
|
String structureKey = decodeCanonicalKey(fields[4]);
|
||||||
|
if (objectKey.isBlank() || structureKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Decoded(objectKey, placementId, structureKey);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Decoded decodeLegacy(String marker) {
|
||||||
|
int separator = marker.indexOf('@');
|
||||||
|
if (separator <= 0 || separator != marker.lastIndexOf('@') || separator == marker.length() - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String objectKey = marker.substring(0, separator);
|
||||||
|
if (objectKey.isEmpty() || objectKey.equals("null")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Decoded(objectKey, Integer.parseInt(marker.substring(separator + 1)), null);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireKey(String key, String name) {
|
||||||
|
String required = Objects.requireNonNull(key, name);
|
||||||
|
if (required.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(name + " must not be blank");
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String encodeKey(String key) {
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decodeCanonicalKey(String key) {
|
||||||
|
String decoded = new String(Base64.getUrlDecoder().decode(key), StandardCharsets.UTF_8);
|
||||||
|
if (!encodeKey(decoded).equals(key)) {
|
||||||
|
throw new IllegalArgumentException("Non-canonical marker key");
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Decoded(String objectKey, int placementId, String structureKey) {
|
||||||
|
public boolean structureAware() {
|
||||||
|
return structureKey != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,6 @@ public class IrisCaveCarver3D {
|
|||||||
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
|
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
|
||||||
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
|
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
|
||||||
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
|
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
|
||||||
private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new);
|
|
||||||
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private final IrisData data;
|
private final IrisData data;
|
||||||
@@ -70,6 +69,7 @@ public class IrisCaveCarver3D {
|
|||||||
private final boolean hasWarp;
|
private final boolean hasWarp;
|
||||||
private final boolean hasModules;
|
private final boolean hasModules;
|
||||||
private final int warpResolution;
|
private final int warpResolution;
|
||||||
|
private final ThreadLocal<Scratch> scratchCache = ThreadLocal.withInitial(Scratch::new);
|
||||||
|
|
||||||
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
|
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -112,7 +112,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
if (!scratch.fullWeightsInitialized) {
|
if (!scratch.fullWeightsInitialized) {
|
||||||
Arrays.fill(scratch.fullWeights, 1D);
|
Arrays.fill(scratch.fullWeights, 1D);
|
||||||
scratch.fullWeightsInitialized = true;
|
scratch.fullWeightsInitialized = true;
|
||||||
@@ -169,7 +169,7 @@ public class IrisCaveCarver3D {
|
|||||||
) {
|
) {
|
||||||
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
|
||||||
try {
|
try {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
if (columnWeights == null || columnWeights.length < 256) {
|
if (columnWeights == null || columnWeights.length < 256) {
|
||||||
if (!scratch.fullWeightsInitialized) {
|
if (!scratch.fullWeightsInitialized) {
|
||||||
Arrays.fill(scratch.fullWeights, 1D);
|
Arrays.fill(scratch.fullWeights, 1D);
|
||||||
@@ -364,7 +364,7 @@ public class IrisCaveCarver3D {
|
|||||||
boolean skipExistingCarved
|
boolean skipExistingCarved
|
||||||
) {
|
) {
|
||||||
int carved = 0;
|
int carved = 0;
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] passThreshold = scratch.passThreshold;
|
double[] passThreshold = scratch.passThreshold;
|
||||||
int[] activeColumnIndices = scratch.activeColumnIndices;
|
int[] activeColumnIndices = scratch.activeColumnIndices;
|
||||||
int[] activeColumnTopY = scratch.activeColumnTopY;
|
int[] activeColumnTopY = scratch.activeColumnTopY;
|
||||||
@@ -483,7 +483,7 @@ public class IrisCaveCarver3D {
|
|||||||
boolean skipExistingCarved
|
boolean skipExistingCarved
|
||||||
) {
|
) {
|
||||||
int carved = 0;
|
int carved = 0;
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] passThreshold = scratch.passThreshold;
|
double[] passThreshold = scratch.passThreshold;
|
||||||
int[] activeColumnIndices = scratch.activeColumnIndices;
|
int[] activeColumnIndices = scratch.activeColumnIndices;
|
||||||
int[] activeColumnTopY = scratch.activeColumnTopY;
|
int[] activeColumnTopY = scratch.activeColumnTopY;
|
||||||
@@ -639,7 +639,7 @@ public class IrisCaveCarver3D {
|
|||||||
boolean skipExistingCarved
|
boolean skipExistingCarved
|
||||||
) {
|
) {
|
||||||
int carved = 0;
|
int carved = 0;
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] passThreshold = scratch.passThreshold;
|
double[] passThreshold = scratch.passThreshold;
|
||||||
int[] tileIndices = scratch.tileIndices;
|
int[] tileIndices = scratch.tileIndices;
|
||||||
int[] tileLocalX = scratch.tileLocalX;
|
int[] tileLocalX = scratch.tileLocalX;
|
||||||
@@ -778,7 +778,7 @@ public class IrisCaveCarver3D {
|
|||||||
boolean skipExistingCarved
|
boolean skipExistingCarved
|
||||||
) {
|
) {
|
||||||
int carved = 0;
|
int carved = 0;
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
|
|
||||||
for (int lx = 0; lx < 16; lx++) {
|
for (int lx = 0; lx < 16; lx++) {
|
||||||
int x = x0 + lx;
|
int x = x0 + lx;
|
||||||
@@ -926,7 +926,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void classifyDensityPlaneNoWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) {
|
private void classifyDensityPlaneNoWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
|
classifyDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
|
||||||
@@ -964,7 +964,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void classifyDensityPlaneWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) {
|
private void classifyDensityPlaneWarpModules(int x0, int z0, int y, int[] planeColumnIndices, double[] planeThresholdLimit, int planeCount, boolean[] planeCarve) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
classifyDensityPlaneWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
|
classifyDensityPlaneWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
|
||||||
@@ -1003,7 +1003,7 @@ public class IrisCaveCarver3D {
|
|||||||
int adaptiveSampleStep,
|
int adaptiveSampleStep,
|
||||||
double adaptiveThresholdMargin
|
double adaptiveThresholdMargin
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity;
|
double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity;
|
||||||
int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep;
|
int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep;
|
||||||
int axisSamples = axisCells + 1;
|
int axisSamples = axisCells + 1;
|
||||||
@@ -1046,7 +1046,7 @@ public class IrisCaveCarver3D {
|
|||||||
int adaptiveSampleStep,
|
int adaptiveSampleStep,
|
||||||
double adaptiveThresholdMargin
|
double adaptiveThresholdMargin
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
classifyDensityPlaneAdaptiveNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
classifyDensityPlaneAdaptiveNoWarpNoModules(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
||||||
@@ -1102,7 +1102,7 @@ public class IrisCaveCarver3D {
|
|||||||
int adaptiveSampleStep,
|
int adaptiveSampleStep,
|
||||||
double adaptiveThresholdMargin
|
double adaptiveThresholdMargin
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity;
|
double[] adaptivePlaneDensity = scratch.adaptivePlaneDensity;
|
||||||
int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep;
|
int axisCells = (16 + adaptiveSampleStep - 1) / adaptiveSampleStep;
|
||||||
int axisSamples = axisCells + 1;
|
int axisSamples = axisCells + 1;
|
||||||
@@ -1145,7 +1145,7 @@ public class IrisCaveCarver3D {
|
|||||||
int adaptiveSampleStep,
|
int adaptiveSampleStep,
|
||||||
double adaptiveThresholdMargin
|
double adaptiveThresholdMargin
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
classifyDensityPlaneAdaptiveWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
classifyDensityPlaneAdaptiveWarpOnly(x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve, adaptiveSampleStep, adaptiveThresholdMargin);
|
||||||
@@ -1211,7 +1211,7 @@ public class IrisCaveCarver3D {
|
|||||||
int axisCells,
|
int axisCells,
|
||||||
int axisSamples
|
int axisSamples
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
||||||
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
||||||
prepareAdaptivePlaneColumns(
|
prepareAdaptivePlaneColumns(
|
||||||
@@ -1271,7 +1271,7 @@ public class IrisCaveCarver3D {
|
|||||||
double[] remainingMin,
|
double[] remainingMin,
|
||||||
double[] remainingMax
|
double[] remainingMax
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
||||||
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
||||||
prepareAdaptivePlaneColumns(
|
prepareAdaptivePlaneColumns(
|
||||||
@@ -1348,7 +1348,7 @@ public class IrisCaveCarver3D {
|
|||||||
int axisCells,
|
int axisCells,
|
||||||
int axisSamples
|
int axisSamples
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
||||||
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
||||||
prepareAdaptivePlaneColumns(
|
prepareAdaptivePlaneColumns(
|
||||||
@@ -1408,7 +1408,7 @@ public class IrisCaveCarver3D {
|
|||||||
double[] remainingMin,
|
double[] remainingMin,
|
||||||
double[] remainingMax
|
double[] remainingMax
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
||||||
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
||||||
prepareAdaptivePlaneColumns(
|
prepareAdaptivePlaneColumns(
|
||||||
@@ -1485,7 +1485,7 @@ public class IrisCaveCarver3D {
|
|||||||
double[] remainingMin,
|
double[] remainingMin,
|
||||||
double[] remainingMax
|
double[] remainingMax
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
double[] adaptivePlanePrediction = scratch.adaptivePlanePrediction;
|
||||||
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
double[] adaptivePlaneAmbiguity = scratch.adaptivePlaneAmbiguity;
|
||||||
prepareAdaptivePlaneColumns(
|
prepareAdaptivePlaneColumns(
|
||||||
@@ -1604,7 +1604,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean classifyDensityPointWarpOnly(int x, int y, int z, double thresholdLimit) {
|
private boolean classifyDensityPointWarpOnly(int x, int y, int z, double thresholdLimit) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int sx = snapWarp(x);
|
int sx = snapWarp(x);
|
||||||
int sy = snapWarp(y);
|
int sy = snapWarp(y);
|
||||||
int sz = snapWarp(z);
|
int sz = snapWarp(z);
|
||||||
@@ -1640,7 +1640,7 @@ public class IrisCaveCarver3D {
|
|||||||
return classifyDensityPointWarpOnly(x, y, z, thresholdLimit);
|
return classifyDensityPointWarpOnly(x, y, z, thresholdLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int sx = snapWarp(x);
|
int sx = snapWarp(x);
|
||||||
int sy = snapWarp(y);
|
int sy = snapWarp(y);
|
||||||
int sz = snapWarp(z);
|
int sz = snapWarp(z);
|
||||||
@@ -1740,7 +1740,7 @@ public class IrisCaveCarver3D {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int sx = snapWarp(x);
|
int sx = snapWarp(x);
|
||||||
int sy = snapWarp(y);
|
int sy = snapWarp(y);
|
||||||
int sz = snapWarp(z);
|
int sz = snapWarp(z);
|
||||||
@@ -1778,7 +1778,7 @@ public class IrisCaveCarver3D {
|
|||||||
int[] adaptivePlaneSampleBounds,
|
int[] adaptivePlaneSampleBounds,
|
||||||
int axisCells
|
int axisCells
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisCells + 1);
|
prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisCells + 1);
|
||||||
int[] adaptiveCellX = scratch.adaptiveCellX;
|
int[] adaptiveCellX = scratch.adaptiveCellX;
|
||||||
int[] adaptiveCellZ = scratch.adaptiveCellZ;
|
int[] adaptiveCellZ = scratch.adaptiveCellZ;
|
||||||
@@ -1822,7 +1822,7 @@ public class IrisCaveCarver3D {
|
|||||||
double[] adaptivePlanePrediction,
|
double[] adaptivePlanePrediction,
|
||||||
double[] adaptivePlaneAmbiguity
|
double[] adaptivePlaneAmbiguity
|
||||||
) {
|
) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples);
|
prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples);
|
||||||
int[] adaptiveCellZ = scratch.adaptiveCellZ;
|
int[] adaptiveCellZ = scratch.adaptiveCellZ;
|
||||||
int[] adaptiveRow0 = scratch.adaptiveRow0;
|
int[] adaptiveRow0 = scratch.adaptiveRow0;
|
||||||
@@ -1882,7 +1882,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double sampleDensityNoWarpModules(int x, int y, int z) {
|
private double sampleDensityNoWarpModules(int x, int y, int z) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
return sampleDensityNoWarpNoModules(x, y, z);
|
return sampleDensityNoWarpNoModules(x, y, z);
|
||||||
@@ -1905,7 +1905,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double sampleDensityWarpOnly(int x, int y, int z) {
|
private double sampleDensityWarpOnly(int x, int y, int z) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int sx = snapWarp(x);
|
int sx = snapWarp(x);
|
||||||
int sy = snapWarp(y);
|
int sy = snapWarp(y);
|
||||||
int sz = snapWarp(z);
|
int sz = snapWarp(z);
|
||||||
@@ -1921,7 +1921,7 @@ public class IrisCaveCarver3D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double sampleDensityWarpModules(int x, int y, int z) {
|
private double sampleDensityWarpModules(int x, int y, int z) {
|
||||||
Scratch scratch = SCRATCH.get();
|
Scratch scratch = scratchCache.get();
|
||||||
int activeModuleCount = prepareActiveModules(scratch, y);
|
int activeModuleCount = prepareActiveModules(scratch, y);
|
||||||
if (activeModuleCount == 0) {
|
if (activeModuleCount == 0) {
|
||||||
return sampleDensityWarpOnly(x, y, z);
|
return sampleDensityWarpOnly(x, y, z);
|
||||||
|
|||||||
+33
-3
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.IrisComplex;
|
|||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||||
|
import art.arcane.iris.engine.framework.StructurePlacementMarker;
|
||||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -37,6 +38,7 @@ import art.arcane.iris.engine.object.IrisRegion;
|
|||||||
import art.arcane.iris.engine.object.IrisStructure;
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
@@ -331,10 +333,10 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
private void placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, ObjectPlaceMode mode, int y, RNG rng) {
|
private void placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, ObjectPlaceMode mode, int y, RNG rng) {
|
||||||
IrisObject object = p.getObject();
|
IrisObject object = p.getObject();
|
||||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
String objectKey = object.getLoadKey();
|
||||||
|
IrisObjectPlacement config = structure.createLootPlacement(objectKey);
|
||||||
config.setMode(mode);
|
config.setMode(mode);
|
||||||
config.setRotation(p.getRotation());
|
config.setRotation(p.getRotation());
|
||||||
config.getPlace().add(object.getLoadKey());
|
|
||||||
if (!structure.getEdit().isEmpty()) {
|
if (!structure.getEdit().isEmpty()) {
|
||||||
config.setEdit(structure.getEdit());
|
config.setEdit(structure.getEdit());
|
||||||
}
|
}
|
||||||
@@ -342,7 +344,35 @@ public class IrisStructureComponent extends IrisMantleComponent {
|
|||||||
config.setForcePlace(true);
|
config.setForcePlace(true);
|
||||||
}
|
}
|
||||||
int placeY = (y == -1) ? -1 : y - getEngineMantle().getEngine().getMinHeight();
|
int placeY = (y == -1) ? -1 : y - getEngineMantle().getEngine().getMinHeight();
|
||||||
object.place(p.getX(), placeY, p.getZ(), writer, config, rng, null, null, getData());
|
String marker = structurePlacementMarker(structure, p, objectKey);
|
||||||
|
object.place(p.getX(), placeY, p.getZ(), writer, config, rng, (position, state) -> {
|
||||||
|
if (marker != null && shouldWriteStructureMarker(state)) {
|
||||||
|
writer.setData(position.getX(), position.getY(), position.getZ(), marker);
|
||||||
|
}
|
||||||
|
}, null, getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldWriteStructureMarker(PlatformBlockState state) {
|
||||||
|
return state != null && state.isStorageChest();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int structurePlacementId(String structureKey, String objectKey, int x, int y, int z) {
|
||||||
|
int hash = 17;
|
||||||
|
hash = 31 * hash + structureKey.hashCode();
|
||||||
|
hash = 31 * hash + objectKey.hashCode();
|
||||||
|
hash = 31 * hash + x;
|
||||||
|
hash = 31 * hash + y;
|
||||||
|
hash = 31 * hash + z;
|
||||||
|
return hash & Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String structurePlacementMarker(IrisStructure structure, PlacedStructurePiece piece, String objectKey) {
|
||||||
|
String structureKey = structure.getLoadKey();
|
||||||
|
if (structureKey == null || structureKey.isBlank() || objectKey == null || objectKey.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int placementId = structurePlacementId(structureKey, objectKey, piece.getX(), piece.getY(), piece.getZ());
|
||||||
|
return StructurePlacementMarker.encodeStructure(objectKey, placementId, structureKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+1
-1
@@ -464,7 +464,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected int computeRadius() {
|
protected int computeRadius() {
|
||||||
return 0;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) {
|
private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) {
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
: resolveCustomBiome(customBiomeCache, customBiome);
|
: resolveCustomBiome(customBiomeCache, customBiome);
|
||||||
|
|
||||||
if (biome != null) {
|
if (biome != null) {
|
||||||
biome.setInferredType(InferredType.CAVE);
|
|
||||||
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
|
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
|
||||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
|
||||||
|
|
||||||
@@ -461,7 +460,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
biome.setInferredType(InferredType.CAVE);
|
|
||||||
|
|
||||||
KList<PlatformBlockState> floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
|
KList<PlatformBlockState> floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
|
||||||
for (int i = 0; i < zoneFloor - 1; i++) {
|
for (int i = 0; i < zoneFloor - 1; i++) {
|
||||||
@@ -550,7 +548,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
biome.setInferredType(InferredType.CAVE);
|
|
||||||
|
|
||||||
KList<PlatformBlockState> blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
|
KList<PlatformBlockState> blocks = biome.generateLayers(getDimension(), xx, zz, rng, 3, zone.floor, getData(), getComplex());
|
||||||
|
|
||||||
@@ -600,9 +597,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
|||||||
|
|
||||||
for (IrisDecorator decorator : biome.getDecorators()) {
|
for (IrisDecorator decorator : biome.getDecorators()) {
|
||||||
if (decorator.getPartOf().equals(IrisDecorationPart.NONE) && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) {
|
if (decorator.getPartOf().equals(IrisDecorationPart.NONE) && zone.getFloor() > 0 && B.isSolid(output.getRaw(rx, zone.getFloor() - 1, rz))) {
|
||||||
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getFloor() - 1, zone.airThickness());
|
decorant.getSurfaceDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getFloor() - 1, zone.airThickness());
|
||||||
} else if (decorator.getPartOf().equals(IrisDecorationPart.CEILING) && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
} else if (decorator.getPartOf().equals(IrisDecorationPart.CEILING) && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) {
|
||||||
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, zone.getCeiling(), zone.airThickness());
|
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, biome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -394,10 +394,10 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
|||||||
|
|
||||||
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
|
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
|
||||||
if (target.isCustom()) {
|
if (target.isCustom()) {
|
||||||
IrisBiomeCustom custom = target.getCustomBiome(rng, wx, 0, wz);
|
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
|
||||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId()));
|
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, wx, 0, wz)));
|
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,14 @@ import art.arcane.volmlib.util.json.JSONObject;
|
|||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
|
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -61,6 +66,8 @@ import java.util.EnumMap;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = false)
|
@EqualsAndHashCode(callSuper = false)
|
||||||
public class IrisBiome extends IrisRegistrant implements IRare {
|
public class IrisBiome extends IrisRegistrant implements IRare {
|
||||||
|
private static final int BIOME_GENERATOR_CACHE_SIZE = 8;
|
||||||
|
|
||||||
private static final class States {
|
private static final class States {
|
||||||
private static final PlatformBlockState BARRIER = B.getState("BARRIER");
|
private static final PlatformBlockState BARRIER = B.getState("BARRIER");
|
||||||
}
|
}
|
||||||
@@ -76,7 +83,13 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
private final transient AtomicCache<Color> cacheColorLayerLoad = new AtomicCache<>();
|
private final transient AtomicCache<Color> cacheColorLayerLoad = new AtomicCache<>();
|
||||||
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
|
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
|
||||||
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
|
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
|
||||||
private final transient AtomicCache<CNG> biomeGenerator = new AtomicCache<>();
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient ConcurrentLinkedHashMap<Long, CNG> biomeGenerators = new ConcurrentLinkedHashMap.Builder<Long, CNG>()
|
||||||
|
.maximumWeightedCapacity(BIOME_GENERATOR_CACHE_SIZE)
|
||||||
|
.build();
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private transient volatile SeededBiomeGenerator recentBiomeGenerator;
|
||||||
private final transient AtomicCache<Integer> maxHeight = new AtomicCache<>();
|
private final transient AtomicCache<Integer> maxHeight = new AtomicCache<>();
|
||||||
private final transient AtomicCache<Integer> maxWithObjectHeight = new AtomicCache<>();
|
private final transient AtomicCache<Integer> maxWithObjectHeight = new AtomicCache<>();
|
||||||
private final transient AtomicCache<IrisBiome> realCarveBiome = new AtomicCache<>();
|
private final transient AtomicCache<IrisBiome> realCarveBiome = new AtomicCache<>();
|
||||||
@@ -469,8 +482,39 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CNG getBiomeGenerator(RNG random) {
|
public CNG getBiomeGenerator(RNG random) {
|
||||||
return biomeGenerator.aquire(() ->
|
IrisData loader = getLoader();
|
||||||
biomeStyle.create(random.nextParallelRNG(213949 + 228888 + getRarity() + getName().length()), getLoader()));
|
Engine engine = resolveBiomeGeneratorEngine(loader);
|
||||||
|
return getBiomeGenerator(random, engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CNG getBiomeGenerator(RNG random, Engine engine) {
|
||||||
|
IrisData loader = getLoader();
|
||||||
|
long seed = engine == null ? random.getSeed() : engine.getSeedManager().getBiome();
|
||||||
|
SeededBiomeGenerator recent = recentBiomeGenerator;
|
||||||
|
if (recent != null && recent.seed == seed) {
|
||||||
|
return recent.generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
CNG generator = biomeGenerators.computeIfAbsent(seed, ignored -> createBiomeGenerator(seed, loader));
|
||||||
|
recentBiomeGenerator = new SeededBiomeGenerator(seed, generator);
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Engine resolveBiomeGeneratorEngine(IrisData loader) {
|
||||||
|
IrisContext context = IrisContext.get();
|
||||||
|
if (context != null) {
|
||||||
|
Engine contextEngine = context.getEngine();
|
||||||
|
if (!contextEngine.isClosed() && (loader == null || contextEngine.getData() == loader)) {
|
||||||
|
return contextEngine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loader == null ? null : loader.getEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CNG createBiomeGenerator(long seed, IrisData loader) {
|
||||||
|
int signature = 213949 + 228888 + getRarity() + getName().length();
|
||||||
|
return biomeStyle.createNoCache(new RNG(seed).nextParallelRNG(signature), loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
|
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
|
||||||
@@ -752,23 +796,31 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Biome getSkyBiome(RNG rng, double x, double y, double z) {
|
public Biome getSkyBiome(RNG rng, double x, double y, double z) {
|
||||||
|
return getSkyBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Biome getSkyBiome(RNG rng, Engine engine, double x, double y, double z) {
|
||||||
if (biomeSkyScatter.size() == 1) {
|
if (biomeSkyScatter.size() == 1) {
|
||||||
return getBiomeSkyScatterResolved().get(0);
|
return getBiomeSkyScatterResolved().get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (biomeSkyScatter.isEmpty()) {
|
if (biomeSkyScatter.isEmpty()) {
|
||||||
return getGroundBiome(rng, x, y, z);
|
return getGroundBiome(rng, engine, x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBiomeSkyScatterResolved().get(getBiomeGenerator(rng).fit(0, biomeSkyScatter.size() - 1, x, y, z));
|
return getBiomeSkyScatterResolved().get(getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) {
|
public IrisBiomeCustom getCustomBiome(RNG rng, double x, double y, double z) {
|
||||||
|
return getCustomBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisBiomeCustom getCustomBiome(RNG rng, Engine engine, double x, double y, double z) {
|
||||||
if (customDerivitives.size() == 1) {
|
if (customDerivitives.size() == 1) {
|
||||||
return customDerivitives.get(0);
|
return customDerivitives.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return customDerivitives.get(getBiomeGenerator(rng).fit(0, customDerivitives.size() - 1, x, y, z));
|
return customDerivitives.get(getBiomeGenerator(rng, engine).fit(0, customDerivitives.size() - 1, x, y, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
public KList<IrisBiome> getRealChildren(DataProvider g) {
|
public KList<IrisBiome> getRealChildren(DataProvider g) {
|
||||||
@@ -801,6 +853,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
|
|
||||||
//TODO: Test
|
//TODO: Test
|
||||||
public Biome getGroundBiome(RNG rng, double x, double y, double z) {
|
public Biome getGroundBiome(RNG rng, double x, double y, double z) {
|
||||||
|
return getGroundBiome(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Biome getGroundBiome(RNG rng, Engine engine, double x, double y, double z) {
|
||||||
if (biomeScatter.isEmpty()) {
|
if (biomeScatter.isEmpty()) {
|
||||||
return getDerivative();
|
return getDerivative();
|
||||||
}
|
}
|
||||||
@@ -809,22 +865,30 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
return getBiomeScatterResolved().get(0);
|
return getBiomeScatterResolved().get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBiomeGenerator(rng).fit(getBiomeScatterResolved(), x, y, z);
|
return getBiomeGenerator(rng, engine).fit(getBiomeScatterResolved(), x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSkyBiomeKey(RNG rng, double x, double y, double z) {
|
public String getSkyBiomeKey(RNG rng, double x, double y, double z) {
|
||||||
|
return getSkyBiomeKey(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSkyBiomeKey(RNG rng, Engine engine, double x, double y, double z) {
|
||||||
if (biomeSkyScatter.size() == 1) {
|
if (biomeSkyScatter.size() == 1) {
|
||||||
return namespacedBiomeKey(biomeSkyScatter.get(0));
|
return namespacedBiomeKey(biomeSkyScatter.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (biomeSkyScatter.isEmpty()) {
|
if (biomeSkyScatter.isEmpty()) {
|
||||||
return getGroundBiomeKey(rng, x, y, z);
|
return getGroundBiomeKey(rng, engine, x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
return namespacedBiomeKey(biomeSkyScatter.get(getBiomeGenerator(rng).fit(0, biomeSkyScatter.size() - 1, x, y, z)));
|
return namespacedBiomeKey(biomeSkyScatter.get(getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGroundBiomeKey(RNG rng, double x, double y, double z) {
|
public String getGroundBiomeKey(RNG rng, double x, double y, double z) {
|
||||||
|
return getGroundBiomeKey(rng, resolveBiomeGeneratorEngine(getLoader()), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroundBiomeKey(RNG rng, Engine engine, double x, double y, double z) {
|
||||||
if (biomeScatter.isEmpty()) {
|
if (biomeScatter.isEmpty()) {
|
||||||
return namespacedBiomeKey(derivative);
|
return namespacedBiomeKey(derivative);
|
||||||
}
|
}
|
||||||
@@ -833,7 +897,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
return namespacedBiomeKey(biomeScatter.get(0));
|
return namespacedBiomeKey(biomeScatter.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return namespacedBiomeKey(biomeScatter.get(getBiomeGenerator(rng).fit(0, biomeScatter.size() - 1, x, y, z)));
|
return namespacedBiomeKey(biomeScatter.get(getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) {
|
public PlatformBlockState getSurfaceBlock(int x, int z, RNG rng, IrisData idm) {
|
||||||
@@ -931,4 +995,14 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
|||||||
public void scanForErrors(JSONObject p, VolmitSender sender) {
|
public void scanForErrors(JSONObject p, VolmitSender sender) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class SeededBiomeGenerator {
|
||||||
|
private final long seed;
|
||||||
|
private final CNG generator;
|
||||||
|
|
||||||
|
private SeededBiomeGenerator(long seed, CNG generator) {
|
||||||
|
this.seed = seed;
|
||||||
|
this.generator = generator;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ public class IrisBiomeCustom {
|
|||||||
@Desc("The biome's mob spawns")
|
@Desc("The biome's mob spawns")
|
||||||
private KList<IrisBiomeCustomSpawn> spawns = new KList<>();
|
private KList<IrisBiomeCustomSpawn> spawns = new KList<>();
|
||||||
|
|
||||||
|
@ArrayType(min = 1, type = String.class)
|
||||||
|
@Desc("Biome tags to opt this custom biome into, such as minecraft:allows_surface_slime_spawns")
|
||||||
|
private KList<String> tags = new KList<>();
|
||||||
|
|
||||||
@Desc("The biome's downfall type")
|
@Desc("The biome's downfall type")
|
||||||
private IrisBiomeCustomPrecipType downfallType = IrisBiomeCustomPrecipType.rain;
|
private IrisBiomeCustomPrecipType downfallType = IrisBiomeCustomPrecipType.rain;
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import lombok.experimental.Accessors;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
@Snippet("custom-biome-spawn")
|
@Snippet("custom-biome-spawn")
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -45,11 +47,12 @@ public class IrisBiomeCustomSpawn {
|
|||||||
private String type = "minecraft:cow";
|
private String type = "minecraft:cow";
|
||||||
|
|
||||||
public EntityType getType() {
|
public EntityType getType() {
|
||||||
if (type == null) {
|
String typeKey = getTypeKey();
|
||||||
|
if (typeKey == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return typeResolved.aquire(() -> {
|
return typeResolved.aquire(() -> {
|
||||||
NamespacedKey namespacedKey = NamespacedKey.fromString(type);
|
NamespacedKey namespacedKey = NamespacedKey.fromString(typeKey);
|
||||||
return namespacedKey == null ? null : RegistryUtil.lookup(EntityType.class).get(namespacedKey);
|
return namespacedKey == null ? null : RegistryUtil.lookup(EntityType.class).get(namespacedKey);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,7 +61,8 @@ public class IrisBiomeCustomSpawn {
|
|||||||
if (type == null || type.isBlank()) {
|
if (type == null || type.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return type.contains(":") ? type : "minecraft:" + type;
|
String normalized = type.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.contains(":") ? normalized : "minecraft:" + normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public enum IrisBiomeCustomSpawnType {
|
|||||||
@Desc("Eg bats")
|
@Desc("Eg bats")
|
||||||
AMBIENT,
|
AMBIENT,
|
||||||
|
|
||||||
|
@Desc("Axolotls")
|
||||||
|
AXOLOTLS,
|
||||||
|
|
||||||
@Desc("Odd spawn group but ok")
|
@Desc("Odd spawn group but ok")
|
||||||
UNDERGROUND_WATER_CREATURE,
|
UNDERGROUND_WATER_CREATURE,
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import art.arcane.volmlib.util.collection.KList;
|
|||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.iris.util.common.data.DataProvider;
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
import art.arcane.volmlib.util.io.IO;
|
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.json.JSONObject;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
import art.arcane.volmlib.util.math.Position2;
|
import art.arcane.volmlib.util.math.Position2;
|
||||||
@@ -56,9 +57,17 @@ import org.bukkit.block.Biome;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -67,6 +76,8 @@ import java.util.Map;
|
|||||||
@EqualsAndHashCode(callSuper = false, doNotUseGetters = true)
|
@EqualsAndHashCode(callSuper = false, doNotUseGetters = true)
|
||||||
@ToString(doNotUseGetters = true)
|
@ToString(doNotUseGetters = true)
|
||||||
public class IrisDimension extends IrisRegistrant {
|
public class IrisDimension extends IrisRegistrant {
|
||||||
|
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||||
|
|
||||||
private final transient AtomicCache<Position2> parallaxSize = new AtomicCache<>();
|
private final transient AtomicCache<Position2> parallaxSize = new AtomicCache<>();
|
||||||
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
|
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
|
||||||
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
|
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
|
||||||
@@ -523,6 +534,7 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
output.getParentFile().mkdirs();
|
output.getParentFile().mkdirs();
|
||||||
try {
|
try {
|
||||||
IO.writeAll(output, json);
|
IO.writeAll(output, json);
|
||||||
|
installBiomeTags(datapacks, namespace + ":" + customBiomeId, customBiome.getTags());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -532,6 +544,97 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void clearGeneratedBiomeTags(KList<File> folders) {
|
||||||
|
for (File datapacks : folders) {
|
||||||
|
File dataFolder = new File(datapacks, "iris/data");
|
||||||
|
File[] namespaces = dataFolder.listFiles(File::isDirectory);
|
||||||
|
if (namespaces == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (File namespace : namespaces) {
|
||||||
|
IO.delete(new File(namespace, "tags/worldgen/biome"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installBiomeTags(File datapacks, String biomeKey, KList<String> tags) throws IOException {
|
||||||
|
if (tags == null || tags.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String rawTag : tags) {
|
||||||
|
String tag = normalizeResourceKey(rawTag);
|
||||||
|
if (tag == null) {
|
||||||
|
IrisLogging.error("Invalid custom biome tag '" + rawTag + "' for " + biomeKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int separator = tag.indexOf(':');
|
||||||
|
String namespace = tag.substring(0, separator);
|
||||||
|
String path = tag.substring(separator + 1);
|
||||||
|
Path tagRoot = new File(datapacks, "iris/data/" + namespace + "/tags/worldgen/biome").toPath()
|
||||||
|
.toAbsolutePath().normalize();
|
||||||
|
Path output = tagRoot.resolve(path + ".json").normalize();
|
||||||
|
if (!output.startsWith(tagRoot)) {
|
||||||
|
IrisLogging.error("Unsafe custom biome tag '" + rawTag + "' for " + biomeKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writeBiomeTag(output, biomeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeResourceKey(String key) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (normalized.indexOf(':') < 0) {
|
||||||
|
normalized = "minecraft:" + normalized;
|
||||||
|
}
|
||||||
|
return RESOURCE_KEY_PATTERN.matcher(normalized).matches() ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBiomeTag(Path output, String biomeKey) throws IOException {
|
||||||
|
synchronized (IrisDimension.class) {
|
||||||
|
Set<String> values = new TreeSet<>();
|
||||||
|
if (Files.isRegularFile(output)) {
|
||||||
|
JSONObject existing = new JSONObject(Files.readString(output, StandardCharsets.UTF_8));
|
||||||
|
JSONArray existingValues = existing.optJSONArray("values");
|
||||||
|
if (existingValues != null) {
|
||||||
|
for (int index = 0; index < existingValues.length(); index++) {
|
||||||
|
String value = existingValues.optString(index, null);
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.add(biomeKey);
|
||||||
|
|
||||||
|
JSONArray outputValues = new JSONArray();
|
||||||
|
for (String value : values) {
|
||||||
|
outputValues.put(value);
|
||||||
|
}
|
||||||
|
JSONObject tag = new JSONObject();
|
||||||
|
tag.put("replace", false);
|
||||||
|
tag.put("values", outputValues);
|
||||||
|
writeAtomic(output, tag.toString(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAtomic(Path output, String content) throws IOException {
|
||||||
|
Files.createDirectories(output.getParent());
|
||||||
|
Path temporary = Files.createTempFile(output.getParent(), output.getFileName().toString(), ".tmp");
|
||||||
|
try {
|
||||||
|
Files.writeString(temporary, content, StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Files.move(temporary, output, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.move(temporary, output, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Dimension getBaseDimension() {
|
public Dimension getBaseDimension() {
|
||||||
return switch (environment) {
|
return switch (environment) {
|
||||||
case NETHER -> Dimension.NETHER;
|
case NETHER -> Dimension.NETHER;
|
||||||
|
|||||||
@@ -71,6 +71,21 @@ public class IrisStructure extends IrisRegistrant {
|
|||||||
@Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.")
|
@Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.")
|
||||||
private String vanillaSource = "";
|
private String vanillaSource = "";
|
||||||
|
|
||||||
|
public IrisObjectPlacement createLootPlacement(String objectKey) {
|
||||||
|
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||||
|
placement.getPlace().add(objectKey);
|
||||||
|
placement.setOverrideGlobalLoot(false);
|
||||||
|
if (loot == null) {
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
for (String lootTable : loot) {
|
||||||
|
if (lootTable != null && !lootTable.isBlank()) {
|
||||||
|
placement.getLoot().add(new IrisObjectLoot().setName(lootTable).setWeight(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFolderName() {
|
public String getFolderName() {
|
||||||
return "structures";
|
return "structures";
|
||||||
|
|||||||
@@ -72,15 +72,6 @@ public class IrisStructurePlacement {
|
|||||||
@Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.")
|
@Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.")
|
||||||
private int ringSpread = 3;
|
private int ringSpread = 3;
|
||||||
|
|
||||||
@Desc("rotation applied to the placed structure.")
|
|
||||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
|
||||||
|
|
||||||
@Desc("translation applied to the placed structure.")
|
|
||||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
|
||||||
|
|
||||||
@Desc("scale applied to the placed structure.")
|
|
||||||
private IrisObjectScale scale = new IrisObjectScale();
|
|
||||||
|
|
||||||
@Desc("When underground=false this is the minimum surface Y the placement is allowed at (a gate); when underground=true this is the lower bound of the Y band the structure is placed within.")
|
@Desc("When underground=false this is the minimum surface Y the placement is allowed at (a gate); when underground=true this is the lower bound of the Y band the structure is placed within.")
|
||||||
private int minHeight = -2032;
|
private int minHeight = -2032;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.platform.bukkit;
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.spi.PlatformEntityType;
|
import art.arcane.iris.spi.PlatformEntityType;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
|
|
||||||
@@ -32,12 +33,14 @@ public final class BukkitEntityType implements PlatformEntityType {
|
|||||||
private final EntityType type;
|
private final EntityType type;
|
||||||
private final String key;
|
private final String key;
|
||||||
private final String namespace;
|
private final String namespace;
|
||||||
|
private final String spawnCategory;
|
||||||
|
|
||||||
private BukkitEntityType(EntityType type, String key) {
|
private BukkitEntityType(EntityType type, String key) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
int colon = key.indexOf(':');
|
int colon = key.indexOf(':');
|
||||||
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
||||||
|
this.spawnCategory = INMS.get().getEntitySpawnCategory(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BukkitEntityType of(EntityType type) {
|
public static BukkitEntityType of(EntityType type) {
|
||||||
@@ -55,6 +58,11 @@ public final class BukkitEntityType implements PlatformEntityType {
|
|||||||
return namespace;
|
return namespace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String spawnCategory() {
|
||||||
|
return spawnCategory;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object nativeHandle() {
|
public Object nativeHandle() {
|
||||||
return type;
|
return type;
|
||||||
|
|||||||
@@ -281,6 +281,41 @@ public class J {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean runGlobal(Runnable runnable) {
|
||||||
|
if (runnable == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BUKKIT_PRESENT) {
|
||||||
|
if (!IrisPlatforms.isBound()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisPlatforms.get().scheduler().global(runnable);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPluginEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolia()) {
|
||||||
|
return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Bukkit.isPrimaryThread()) {
|
||||||
|
runnable.run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bukkit.getScheduler().runTask(BukkitPlatform.plugin(), runnable);
|
||||||
|
return true;
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
FoliaScheduler.forceFoliaThreading(Bukkit.getServer());
|
||||||
|
return FoliaScheduler.runGlobal(BukkitPlatform.plugin(), runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) {
|
public static boolean runRegion(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) {
|
||||||
if (world == null || runnable == null) {
|
if (world == null || runnable == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package art.arcane.iris.core.nms.datapack.v1217;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class DataFixerV1217ClockTest {
|
||||||
|
@Test
|
||||||
|
public void overworldProvidesTheVanillaDefaultClock() {
|
||||||
|
JSONObject json = fixed(Dimension.OVERWORLD);
|
||||||
|
|
||||||
|
assertEquals("minecraft:overworld", json.getString("default_clock"));
|
||||||
|
assertFalse(json.optBoolean("has_fixed_time", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void endProvidesItsFixedVanillaClock() {
|
||||||
|
JSONObject json = fixed(Dimension.END);
|
||||||
|
|
||||||
|
assertEquals("minecraft:the_end", json.getString("default_clock"));
|
||||||
|
assertTrue(json.getBoolean("has_fixed_time"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void netherRemainsFixedWithoutADefaultClock() {
|
||||||
|
JSONObject json = fixed(Dimension.NETHER);
|
||||||
|
|
||||||
|
assertFalse(json.has("default_clock"));
|
||||||
|
assertTrue(json.getBoolean("has_fixed_time"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject fixed(Dimension dimension) {
|
||||||
|
DataFixerV1217 fixer = new DataFixerV1217();
|
||||||
|
JSONObject json = fixer.resolve(dimension, null);
|
||||||
|
fixer.fixDimension(dimension, json);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
public class PackDirectoryResolverTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesOnlyExistingDirectChildren() throws Exception {
|
||||||
|
File packs = temporaryFolder.newFolder("packs");
|
||||||
|
File overworld = new File(packs, "overworld");
|
||||||
|
Files.createDirectory(overworld.toPath());
|
||||||
|
|
||||||
|
assertEquals(overworld.getAbsoluteFile(), PackDirectoryResolver.resolveExisting(packs, "overworld"));
|
||||||
|
assertEquals(overworld.getAbsoluteFile(), PackDirectoryResolver.resolveExisting(packs, "./overworld"));
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, "missing"));
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsTraversalAbsoluteAndNestedPaths() throws Exception {
|
||||||
|
File packs = temporaryFolder.newFolder("pack-root");
|
||||||
|
File outside = temporaryFolder.newFolder("outside");
|
||||||
|
File nested = new File(packs, "nested/pack");
|
||||||
|
Files.createDirectories(nested.toPath());
|
||||||
|
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, "../outside"));
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, outside.getAbsolutePath()));
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, "nested/pack"));
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsSymbolicLinkChildren() throws Exception {
|
||||||
|
File packs = temporaryFolder.newFolder("symlink-root");
|
||||||
|
File outside = temporaryFolder.newFolder("symlink-target");
|
||||||
|
Path link = new File(packs, "linked").toPath();
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(link, outside.toPath());
|
||||||
|
} catch (IOException | UnsupportedOperationException | SecurityException e) {
|
||||||
|
Assume.assumeNoException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(PackDirectoryResolver.resolveExisting(packs, "linked"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
public class PackDownloaderTest {
|
||||||
|
@Test
|
||||||
|
public void resolvesBranchReference() {
|
||||||
|
assertEquals(
|
||||||
|
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/heads/feature/release",
|
||||||
|
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "feature/release")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesQualifiedHeadReference() {
|
||||||
|
assertEquals(
|
||||||
|
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/heads/master",
|
||||||
|
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/heads/master")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesTagReference() {
|
||||||
|
assertEquals(
|
||||||
|
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/tags/v4.0.0",
|
||||||
|
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/tags/v4.0.0")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesCommitReference() {
|
||||||
|
assertEquals(
|
||||||
|
"https://github.com/IrisDimensions/overworld/archive/8e32852ee6ecd039fae27a36f701f57cdc02e83f.zip",
|
||||||
|
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "8e32852ee6ecd039fae27a36f701f57cdc02e83f")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnsafeRepositoryAndReference() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld?raw=1", "master"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("../overworld", "master"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/heads/../master"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/pull/123/head"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class PackResourceCleanupTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void previewIsReadOnlyAndReturnsSortedCandidates() throws Exception {
|
||||||
|
File pack = createPack("preview");
|
||||||
|
write(pack, "dimensions/main.json", "{\"biome\":\"nested/used\"}");
|
||||||
|
write(pack, "biomes/z-last.json", "{}");
|
||||||
|
write(pack, "biomes/a-first.json", "{}");
|
||||||
|
write(pack, "biomes/nested/used.json", "{}");
|
||||||
|
|
||||||
|
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(pack);
|
||||||
|
|
||||||
|
assertTrue(preview.success());
|
||||||
|
assertTrue(preview.hasCandidates());
|
||||||
|
assertEquals(List.of("biomes/a-first.json", "biomes/z-last.json"), preview.candidatePaths());
|
||||||
|
assertFalse(new File(pack, ".iris-trash").exists());
|
||||||
|
assertTrue(new File(pack, "biomes/a-first.json").isFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void applyRunsFreshScanBeforeMutating() throws Exception {
|
||||||
|
File pack = createPack("fresh-scan");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/kept.json", "{}");
|
||||||
|
|
||||||
|
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(pack);
|
||||||
|
assertEquals(List.of("biomes/kept.json"), preview.candidatePaths());
|
||||||
|
|
||||||
|
write(pack, "dimensions/main.json", "{\"biome\":\"kept\"}");
|
||||||
|
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(pack);
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
assertFalse(result.changed());
|
||||||
|
assertNull(result.quarantinePath());
|
||||||
|
assertTrue(new File(pack, "biomes/kept.json").isFile());
|
||||||
|
assertFalse(new File(pack, ".iris-trash").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void applyCreatesUniqueDumpsAndPreservesRelativePaths() throws Exception {
|
||||||
|
File pack = createPack("unique-dumps");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/nested/first.json", "{\"value\":1}");
|
||||||
|
|
||||||
|
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
|
||||||
|
assertTrue(first.success());
|
||||||
|
assertEquals(List.of("biomes/nested/first.json"), first.quarantinedPaths());
|
||||||
|
assertTrue(new File(pack, first.quarantinePath() + "/biomes/nested/first.json").isFile());
|
||||||
|
|
||||||
|
write(pack, "biomes/nested/second.json", "{\"value\":2}");
|
||||||
|
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
|
||||||
|
|
||||||
|
assertTrue(second.success());
|
||||||
|
assertNotEquals(first.quarantinePath(), second.quarantinePath());
|
||||||
|
assertTrue(new File(pack, first.quarantinePath() + "/biomes/nested/first.json").isFile());
|
||||||
|
assertTrue(new File(pack, second.quarantinePath() + "/biomes/nested/second.json").isFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restorePreviewReportsLatestDumpAndAllConflictsWithoutMutation() throws Exception {
|
||||||
|
File pack = createPack("restore-conflict");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/first.json", "{\"value\":1}");
|
||||||
|
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
|
||||||
|
write(pack, "biomes/second.json", "{\"value\":2}");
|
||||||
|
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
|
||||||
|
write(pack, "biomes/second.json", "{\"replacement\":true}");
|
||||||
|
|
||||||
|
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(pack);
|
||||||
|
|
||||||
|
assertTrue(preview.success());
|
||||||
|
assertEquals(second.quarantinePath(), preview.dumpPath());
|
||||||
|
assertEquals(List.of("biomes/second.json"), preview.filePaths());
|
||||||
|
assertEquals(List.of("biomes/second.json"), preview.conflicts());
|
||||||
|
assertTrue(preview.hasConflicts());
|
||||||
|
assertFalse(preview.canRestore());
|
||||||
|
assertTrue(new File(pack, second.quarantinePath() + "/biomes/second.json").isFile());
|
||||||
|
assertTrue(new File(pack, first.quarantinePath() + "/biomes/first.json").isFile());
|
||||||
|
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
|
||||||
|
|
||||||
|
assertFalse(result.success());
|
||||||
|
assertEquals(List.of("biomes/second.json"), result.conflicts());
|
||||||
|
assertTrue(new File(pack, second.quarantinePath() + "/biomes/second.json").isFile());
|
||||||
|
assertEquals("{\"replacement\":true}", read(pack, "biomes/second.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreLatestRestoresOnlyNewestDump() throws Exception {
|
||||||
|
File pack = createPack("restore-latest");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/first.json", "{\"value\":1}");
|
||||||
|
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
|
||||||
|
write(pack, "biomes/second.json", "{\"value\":2}");
|
||||||
|
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
|
||||||
|
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
assertTrue(result.changed());
|
||||||
|
assertEquals(second.quarantinePath(), result.dumpPath());
|
||||||
|
assertEquals(List.of("biomes/second.json"), result.restoredPaths());
|
||||||
|
assertEquals("{\"value\":2}", read(pack, "biomes/second.json"));
|
||||||
|
assertFalse(new File(pack, "biomes/first.json").exists());
|
||||||
|
assertTrue(new File(pack, first.quarantinePath() + "/biomes/first.json").isFile());
|
||||||
|
assertFalse(new File(pack, second.quarantinePath()).exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreCopyFailureRollsBackCreatedDestinations() throws Exception {
|
||||||
|
File pack = createPack("restore-rollback");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/a-readable.json", "{\"value\":1}");
|
||||||
|
write(pack, "biomes/z-unreadable.json", "{\"value\":2}");
|
||||||
|
PackResourceCleanup.ApplyResult applied = PackResourceCleanup.apply(pack);
|
||||||
|
Path unreadable = new File(pack, applied.quarantinePath() + "/biomes/z-unreadable.json").toPath();
|
||||||
|
Assume.assumeTrue(Files.getFileStore(unreadable).supportsFileAttributeView("posix"));
|
||||||
|
Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(unreadable);
|
||||||
|
Files.setPosixFilePermissions(unreadable, Set.of(PosixFilePermission.OWNER_WRITE));
|
||||||
|
try {
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
|
||||||
|
|
||||||
|
assertFalse(result.success());
|
||||||
|
assertTrue(result.error().contains("rolled back"));
|
||||||
|
assertFalse(new File(pack, "biomes/a-readable.json").exists());
|
||||||
|
assertFalse(new File(pack, "biomes/z-unreadable.json").exists());
|
||||||
|
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/a-readable.json").isFile());
|
||||||
|
assertTrue(Files.exists(unreadable));
|
||||||
|
} finally {
|
||||||
|
Files.setPosixFilePermissions(unreadable, originalPermissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreSourceRemovalFailureRollsBackDestinationsAndPreservesDump() throws Exception {
|
||||||
|
File pack = createPack("restore-delete-rollback");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/a.json", "{\"value\":1}");
|
||||||
|
write(pack, "biomes/b.json", "{\"value\":2}");
|
||||||
|
PackResourceCleanup.ApplyResult applied = PackResourceCleanup.apply(pack);
|
||||||
|
Path quarantineBiomes = new File(pack, applied.quarantinePath() + "/biomes").toPath();
|
||||||
|
Assume.assumeTrue(Files.getFileStore(quarantineBiomes).supportsFileAttributeView("posix"));
|
||||||
|
Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(quarantineBiomes);
|
||||||
|
Files.setPosixFilePermissions(quarantineBiomes, Set.of(
|
||||||
|
PosixFilePermission.OWNER_READ,
|
||||||
|
PosixFilePermission.OWNER_EXECUTE));
|
||||||
|
try {
|
||||||
|
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
|
||||||
|
|
||||||
|
assertFalse(result.success());
|
||||||
|
assertTrue(result.error().contains("source removal failed"));
|
||||||
|
assertFalse(new File(pack, "biomes/a.json").exists());
|
||||||
|
assertFalse(new File(pack, "biomes/b.json").exists());
|
||||||
|
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/a.json").isFile());
|
||||||
|
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/b.json").isFile());
|
||||||
|
} finally {
|
||||||
|
Files.setPosixFilePermissions(quarantineBiomes, originalPermissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cleanupFailsClosedWhenManagedContentContainsSymbolicLink() throws Exception {
|
||||||
|
File pack = createPack("symlink");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
File outside = temporaryFolder.newFile("outside.json");
|
||||||
|
Path link = new File(pack, "biomes/linked.json").toPath();
|
||||||
|
Files.createDirectories(link.getParent());
|
||||||
|
try {
|
||||||
|
Files.createSymbolicLink(link, outside.toPath());
|
||||||
|
} catch (IOException | UnsupportedOperationException | SecurityException e) {
|
||||||
|
Assume.assumeNoException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(pack);
|
||||||
|
|
||||||
|
assertFalse(result.success());
|
||||||
|
assertFalse(result.changed());
|
||||||
|
assertTrue(Files.isSymbolicLink(link));
|
||||||
|
assertFalse(new File(pack, ".iris-trash").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentAppliesSerializePerPack() throws Exception {
|
||||||
|
File pack = createPack("concurrent");
|
||||||
|
write(pack, "dimensions/main.json", "{}");
|
||||||
|
write(pack, "biomes/unused.json", "{}");
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<PackResourceCleanup.ApplyResult> first = executor.submit(() -> {
|
||||||
|
start.await();
|
||||||
|
return PackResourceCleanup.apply(pack);
|
||||||
|
});
|
||||||
|
Future<PackResourceCleanup.ApplyResult> second = executor.submit(() -> {
|
||||||
|
start.await();
|
||||||
|
return PackResourceCleanup.apply(pack);
|
||||||
|
});
|
||||||
|
start.countDown();
|
||||||
|
|
||||||
|
List<PackResourceCleanup.ApplyResult> results = List.of(first.get(), second.get());
|
||||||
|
assertTrue(results.stream().allMatch(PackResourceCleanup.ApplyResult::success));
|
||||||
|
assertEquals(1L, results.stream().filter(PackResourceCleanup.ApplyResult::changed).count());
|
||||||
|
assertFalse(new File(pack, "biomes/unused.json").exists());
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createPack(String name) throws Exception {
|
||||||
|
return temporaryFolder.newFolder(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(File pack, String relativePath, String content) throws Exception {
|
||||||
|
Path path = new File(pack, relativePath).toPath();
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String read(File pack, String relativePath) throws Exception {
|
||||||
|
return Files.readString(new File(pack, relativePath).toPath(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class PackValidatorCustomBiomeSpawnTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsExplicitMatchingSpawnGroup() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsBiomeWithoutCustomSpawns() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"name\":\"Plains\"}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMissingSpawnGroup() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("must declare group 'MONSTER'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsImplicitMiscSpawnGroup() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"effects\",\"spawns\":[{\"type\":\"minecraft:armor_stand\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("misc"));
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsSpawnGroupThatDisagreesWithRegistry() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MISC\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("live entity registry requires 'MONSTER'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsAxolotlSpawnCategory() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"cave\",\"spawns\":[{\"type\":\"minecraft:axolotl\",\"group\":\"AXOLOTLS\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("axolotls"));
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnknownSpawnEntity() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"missing:entity\",\"group\":\"MISC\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.unknown());
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("unknown entity type 'missing:entity'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsCaseNormalizedGroupThatRuntimeWouldNotParse() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"monster\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(
|
||||||
|
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("unknown group 'monster'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsWrongCustomDerivativeContainerType() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":{}}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("customDerivitives must be an array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsWrongSpawnContainerType() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":{}}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("spawns must be an array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsNullCustomDerivativeContainerAsAbsent() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":null}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsNullSpawnContainerAsAbsent() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":null}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsNamespacedCustomBiomeTags() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:allows_surface_slime_spawns\"]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnsafeCustomBiomeTags() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:../outside\"]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("invalid tag"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convertsSpawnCategoryResolverFailureIntoBlockingError() throws Exception {
|
||||||
|
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, key -> {
|
||||||
|
throw new IllegalStateException("registry unavailable");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("spawn category lookup failed"));
|
||||||
|
assertTrue(errors.get(0).contains("registry unavailable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createBiomes(String json) throws Exception {
|
||||||
|
File biomes = temporaryFolder.newFolder("biomes");
|
||||||
|
File biome = new File(biomes, "test.json");
|
||||||
|
Files.writeString(biome.toPath(), json, StandardCharsets.UTF_8);
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class PackValidatorReadOnlyTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validationDoesNotMoveOrRewritePackFiles() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
|
||||||
|
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||||
|
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||||
|
write(pack, "generators/unused.json", "{\"name\":\"Unused\"}");
|
||||||
|
Map<String, byte[]> before = snapshot(pack.toPath());
|
||||||
|
|
||||||
|
PackValidationResult result = PackValidator.validate(pack);
|
||||||
|
|
||||||
|
assertTrue(result.isLoadable());
|
||||||
|
assertEquals(before.keySet(), snapshot(pack.toPath()).keySet());
|
||||||
|
Map<String, byte[]> after = snapshot(pack.toPath());
|
||||||
|
for (Map.Entry<String, byte[]> entry : before.entrySet()) {
|
||||||
|
assertArrayEquals(entry.getValue(), after.get(entry.getKey()));
|
||||||
|
}
|
||||||
|
assertFalse(new File(pack, ".iris-trash").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(File root, String relative, String content) throws Exception {
|
||||||
|
Path path = root.toPath().resolve(relative);
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, byte[]> snapshot(Path root) throws Exception {
|
||||||
|
Map<String, byte[]> files = new LinkedHashMap<>();
|
||||||
|
try (Stream<Path> stream = Files.walk(root)) {
|
||||||
|
for (Path path : stream.filter(Files::isRegularFile).sorted().toList()) {
|
||||||
|
files.put(root.relativize(path).toString(), Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class PackValidatorSpawnerEntityTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsNestedEntitiesReferencedByBothSpawnLists() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/frozen/passive.json", """
|
||||||
|
{
|
||||||
|
"spawns": [{"entity": "standard/passive/cod"}],
|
||||||
|
"initialSpawns": [{"entity": "standard/passive/camel"}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
write(pack, "entities/standard/passive/cod.json", "{\"type\":\"COD\",\"surface\":\"WATER\"}");
|
||||||
|
write(pack, "entities/standard/passive/camel.json", "{\"type\":\"CAMEL\"}");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMalformedContainersAndEntries() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/broken.json", """
|
||||||
|
{
|
||||||
|
"spawns": null,
|
||||||
|
"initialSpawns": [7, {}, {"entity": 9}, {"entity": " "}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertEquals(5, errors.size());
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("spawns must be an array")));
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("non-object entry")));
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("without an entity reference")));
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("must be a string")));
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("blank entity reference")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMissingAndMalformedReferencedEntities() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/broken.json", """
|
||||||
|
{
|
||||||
|
"spawns": [
|
||||||
|
{"entity": "standard/hostile/missing"},
|
||||||
|
{"entity": "standard/hostile/broken"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
write(pack, "entities/standard/hostile/broken.json", "not-json");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertEquals(2, errors.size());
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("references missing entity 'standard/hostile/missing'")));
|
||||||
|
assertTrue(errors.stream().anyMatch(error -> error.contains("references malformed entity 'standard/hostile/broken'")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMalformedSpawnerJson() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/broken.json", "not-json");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("Spawner 'broken' has invalid JSON"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsEntityReferenceThatEscapesPack() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/broken.json", "{\"spawns\":[{\"entity\":\"../outside\"}]}");
|
||||||
|
write(pack, "outside.json", "{\"type\":\"COW\"}");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertTrue(errors.get(0).contains("unsafe entity reference '../outside'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ignoresMalformedEntityThatNoSpawnerReferences() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "spawners/passive.json", "{\"spawns\":[{\"entity\":\"standard/passive/cow\"}]}");
|
||||||
|
write(pack, "entities/standard/passive/cow.json", "{\"type\":\"COW\"}");
|
||||||
|
write(pack, "entities/unique/unused.json", "not-json");
|
||||||
|
|
||||||
|
List<String> errors = validate(pack);
|
||||||
|
|
||||||
|
assertTrue(errors.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> validate(File pack) {
|
||||||
|
return PackValidator.validateSpawnerEntityReferences(
|
||||||
|
new File(pack, "spawners"), new File(pack, "entities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(File root, String relative, String content) throws Exception {
|
||||||
|
Path path = root.toPath().resolve(relative);
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package art.arcane.iris.core.pack;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class PackValidatorStructureTransformTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reportsUnsupportedTransformsAcrossEveryStructureHost() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"test\"],\"rotation\":{}}]}");
|
||||||
|
write(pack, "regions/nested/region.json", "{\"structures\":[{\"structures\":[\"test\"],\"translate\":{}}]}");
|
||||||
|
write(pack, "biomes/nested/biome.json", "{\"structures\":[{\"structures\":[\"test\"],\"scale\":{}}]}");
|
||||||
|
|
||||||
|
List<String> errors = PackValidator.validateUnsupportedStructureTransforms(pack);
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
"Dimension 'main' structures[0] declares unsupported field 'rotation'. Structure placement transforms are not supported; remove the field.",
|
||||||
|
"Region 'nested/region' structures[0] declares unsupported field 'translate'. Structure placement transforms are not supported; remove the field.",
|
||||||
|
"Biome 'nested/biome' structures[0] declares unsupported field 'scale'. Structure placement transforms are not supported; remove the field."
|
||||||
|
), errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ignoresTransformsOutsideStructurePlacementArrays() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "biomes/biome.json", "{\"objects\":[{\"rotation\":{},\"translate\":{},\"scale\":{}}],\"structures\":[{\"structures\":[\"test\"]}]}");
|
||||||
|
|
||||||
|
assertTrue(PackValidator.validateUnsupportedStructureTransforms(pack).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedTransformBlocksFullPackValidation() throws Exception {
|
||||||
|
File pack = temporaryFolder.newFolder("pack");
|
||||||
|
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"],\"structures\":[{\"structures\":[\"test\"],\"rotation\":null}]}");
|
||||||
|
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
|
||||||
|
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
|
||||||
|
|
||||||
|
PackValidationResult result = PackValidator.validate(pack);
|
||||||
|
|
||||||
|
assertFalse(result.isLoadable());
|
||||||
|
assertTrue(result.getBlockingErrors().contains(
|
||||||
|
"Dimension 'main' structures[0] declares unsupported field 'rotation'. Structure placement transforms are not supported; remove the field."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(File root, String relative, String content) throws Exception {
|
||||||
|
Path path = root.toPath().resolve(relative);
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@ import java.io.PrintStream;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class IrisPregeneratorInitTest {
|
public class IrisPregeneratorInitTest {
|
||||||
@@ -53,6 +55,35 @@ public class IrisPregeneratorInitTest {
|
|||||||
assertTrue(output.contains("Pregen finished: generated=8 total=9 failed=1"));
|
assertTrue(output.contains("Pregen finished: generated=8 total=9 failed=1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancellationSummaryDoesNotReportPartialRunAsFinished() {
|
||||||
|
IrisSettings previousSettings = IrisSettings.settings;
|
||||||
|
PrintStream previousOut = System.out;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||||
|
AtomicReference<IrisPregenerator> pregeneratorReference = new AtomicReference<>();
|
||||||
|
CancelAfterFirstChunkMethod method = new CancelAfterFirstChunkMethod(pregeneratorReference);
|
||||||
|
PregenTask task = PregenTask.builder()
|
||||||
|
.center(new Position2(0, 0))
|
||||||
|
.radiusX(1)
|
||||||
|
.radiusZ(1)
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
IrisPregenerator pregenerator = new IrisPregenerator(task, method, new NoOpPregenListener());
|
||||||
|
pregeneratorReference.set(pregenerator);
|
||||||
|
|
||||||
|
pregenerator.start();
|
||||||
|
|
||||||
|
String summary = output.toString(StandardCharsets.UTF_8);
|
||||||
|
assertTrue(summary.contains("Pregen cancelled: generated=1 total=9 failed=0 remaining=8"));
|
||||||
|
assertFalse(summary.contains("Pregen finished:"));
|
||||||
|
} finally {
|
||||||
|
System.setOut(previousOut);
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String runCompletionOnClose(boolean failLast) {
|
private String runCompletionOnClose(boolean failLast) {
|
||||||
IrisSettings previousSettings = IrisSettings.settings;
|
IrisSettings previousSettings = IrisSettings.settings;
|
||||||
PrintStream previousOut = System.out;
|
PrintStream previousOut = System.out;
|
||||||
@@ -173,6 +204,55 @@ public class IrisPregeneratorInitTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CancelAfterFirstChunkMethod implements PregeneratorMethod {
|
||||||
|
private final AtomicReference<IrisPregenerator> pregeneratorReference;
|
||||||
|
private boolean cancelled;
|
||||||
|
|
||||||
|
private CancelAfterFirstChunkMethod(AtomicReference<IrisPregenerator> pregeneratorReference) {
|
||||||
|
this.pregeneratorReference = pregeneratorReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRegions(int x, int z, PregenListener listener) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMethod(int x, int z) {
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generateRegion(int x, int z, PregenListener listener) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generateChunk(int x, int z, PregenListener listener) {
|
||||||
|
listener.onChunkGenerated(x, z);
|
||||||
|
if (!cancelled) {
|
||||||
|
cancelled = true;
|
||||||
|
pregeneratorReference.get().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mantle getMantle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class NoOpPregenListener implements PregenListener {
|
private static final class NoOpPregenListener implements PregenListener {
|
||||||
@Override
|
@Override
|
||||||
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
|
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package art.arcane.iris.core.project;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||||
|
import art.arcane.iris.engine.object.IrisSpawner;
|
||||||
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisProjectEntityDependencyTest {
|
||||||
|
@Test
|
||||||
|
public void collectsInitialOnlyEntityAlongsideNormalSpawnEntityForExport() {
|
||||||
|
IrisSpawner spawner = new IrisSpawner();
|
||||||
|
spawner.getSpawns().add(new IrisEntitySpawn().setEntity("standard/passive/cow"));
|
||||||
|
spawner.getInitialSpawns().add(new IrisEntitySpawn().setEntity("beta/sentinel"));
|
||||||
|
KSet<IrisSpawner> spawners = new KSet<>();
|
||||||
|
spawners.add(spawner);
|
||||||
|
|
||||||
|
KSet<String> entityKeys = IrisProject.collectSpawnerEntityKeys(spawners);
|
||||||
|
|
||||||
|
assertEquals(2, entityKeys.size());
|
||||||
|
assertTrue(entityKeys.contains("standard/passive/cow"));
|
||||||
|
assertTrue(entityKeys.contains("beta/sentinel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void returnsNoEntityDependenciesForEmptySpawnerSet() {
|
||||||
|
KSet<String> entityKeys = IrisProject.collectSpawnerEntityKeys(new KSet<>());
|
||||||
|
|
||||||
|
assertTrue(entityKeys.isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
package art.arcane.iris.core.project;
|
package art.arcane.iris.core.project;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
import art.arcane.iris.engine.object.annotations.Desc;
|
import art.arcane.iris.engine.object.annotations.Desc;
|
||||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||||
@@ -50,6 +54,10 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class SchemaBuilderParityTest {
|
public class SchemaBuilderParityTest {
|
||||||
private static final List<String> POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
|
private static final List<String> POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
|
||||||
@@ -108,6 +116,26 @@ public class SchemaBuilderParityTest {
|
|||||||
assertEquals(List.of("ALPHA", "BETA"), enumValues(schema.getJSONObject("definitions"), flavorDefinitionKey()));
|
assertEquals(List.of("ALPHA", "BETA"), enumValues(schema.getJSONObject("definitions"), flavorDefinitionKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structurePlacementSchemaOmitsUnsupportedTransforms() {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
|
||||||
|
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
|
||||||
|
when(data.getStructureLoader()).thenReturn(structureLoader);
|
||||||
|
when(data.getJigsawPieceLoader()).thenReturn(pieceLoader);
|
||||||
|
when(structureLoader.getPossibleKeys()).thenReturn(new String[0]);
|
||||||
|
when(pieceLoader.getPossibleKeys()).thenReturn(new String[0]);
|
||||||
|
|
||||||
|
JSONObject properties = new SchemaBuilder(IrisStructurePlacement.class, data)
|
||||||
|
.construct().getJSONObject("properties");
|
||||||
|
|
||||||
|
assertTrue(properties.has("structures"));
|
||||||
|
assertTrue(properties.has("distribution"));
|
||||||
|
assertFalse(properties.has("rotation"));
|
||||||
|
assertFalse(properties.has("translate"));
|
||||||
|
assertFalse(properties.has("scale"));
|
||||||
|
}
|
||||||
|
|
||||||
private static String flavorDefinitionKey() {
|
private static String flavorDefinitionKey() {
|
||||||
return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package art.arcane.iris.core.tools;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisToolbeltPackReferenceTest {
|
||||||
|
@Test
|
||||||
|
public void plainPackUsesMatchingDimensionKey() {
|
||||||
|
IrisToolbelt.PackReference reference = IrisToolbelt.parsePackReference("overworld");
|
||||||
|
|
||||||
|
assertEquals("overworld", reference.pack());
|
||||||
|
assertEquals("overworld", reference.dimension());
|
||||||
|
assertFalse(reference.explicitDimension());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void explicitDimensionKeepsPackAndDimensionSeparate() {
|
||||||
|
IrisToolbelt.PackReference reference = IrisToolbelt.parsePackReference(" custom_pack : dimensions/sky ");
|
||||||
|
|
||||||
|
assertEquals("custom_pack", reference.pack());
|
||||||
|
assertEquals("dimensions/sky", reference.dimension());
|
||||||
|
assertTrue(reference.explicitDimension());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedReferencesAreRejected() {
|
||||||
|
assertNull(IrisToolbelt.parsePackReference(null));
|
||||||
|
assertNull(IrisToolbelt.parsePackReference(""));
|
||||||
|
assertNull(IrisToolbelt.parsePackReference(":"));
|
||||||
|
assertNull(IrisToolbelt.parsePackReference("pack:"));
|
||||||
|
assertNull(IrisToolbelt.parsePackReference(":dimension"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
|
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||||
|
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
|
||||||
|
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBoundsProvider;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class IrisComplexGridBoundsCacheTest {
|
||||||
|
@Test
|
||||||
|
public void gridBoundsCacheIsIsolatedPerComplex() throws Exception {
|
||||||
|
IrisComplex first = createComplex();
|
||||||
|
IrisComplex second = createComplex();
|
||||||
|
Method cornerBounds = cornerBoundsMethod();
|
||||||
|
|
||||||
|
long firstPacked = invokeCornerBounds(first, cornerBounds, new CountingInterpolator(1.25D, 2.5D), 64, -32);
|
||||||
|
long secondPacked = invokeCornerBounds(second, cornerBounds, new CountingInterpolator(10.25D, 20.5D), 64, -32);
|
||||||
|
|
||||||
|
assertNotEquals(firstPacked, secondPacked);
|
||||||
|
assertEquals(1.25F, unpackLow(firstPacked), 0D);
|
||||||
|
assertEquals(2.5F, unpackHigh(firstPacked), 0D);
|
||||||
|
assertEquals(10.25F, unpackLow(secondPacked), 0D);
|
||||||
|
assertEquals(20.5F, unpackHigh(secondPacked), 0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void gridBoundsCacheReusesCornersWithinSameComplex() throws Exception {
|
||||||
|
IrisComplex complex = createComplex();
|
||||||
|
Method cornerBounds = cornerBoundsMethod();
|
||||||
|
CountingInterpolator interpolator = new CountingInterpolator(3.5D, 7.25D);
|
||||||
|
|
||||||
|
long firstPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
|
||||||
|
long secondPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
|
||||||
|
|
||||||
|
assertEquals(firstPacked, secondPacked);
|
||||||
|
assertEquals(1, interpolator.getInvocations());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisComplex createComplex() throws Exception {
|
||||||
|
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||||
|
|
||||||
|
Field generatorBounds = IrisComplex.class.getDeclaredField("generatorBounds");
|
||||||
|
generatorBounds.setAccessible(true);
|
||||||
|
generatorBounds.set(complex, new HashMap<>());
|
||||||
|
|
||||||
|
Class<?> cacheClass = Class.forName("art.arcane.iris.engine.IrisComplex$GridBoundsCache");
|
||||||
|
Constructor<?> cacheConstructor = cacheClass.getDeclaredConstructor();
|
||||||
|
cacheConstructor.setAccessible(true);
|
||||||
|
ThreadLocal<Object> cache = ThreadLocal.withInitial(() -> newCache(cacheConstructor));
|
||||||
|
Field gridBoundsCache = IrisComplex.class.getDeclaredField("gridBoundsCache");
|
||||||
|
gridBoundsCache.setAccessible(true);
|
||||||
|
gridBoundsCache.set(complex, cache);
|
||||||
|
return complex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object newCache(Constructor<?> constructor) {
|
||||||
|
try {
|
||||||
|
return constructor.newInstance();
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method cornerBoundsMethod() throws Exception {
|
||||||
|
Class<?> cacheClass = Class.forName("art.arcane.iris.engine.IrisComplex$GridBoundsCache");
|
||||||
|
Method method = IrisComplex.class.getDeclaredMethod(
|
||||||
|
"cornerBounds",
|
||||||
|
cacheClass,
|
||||||
|
Engine.class,
|
||||||
|
IrisInterpolator.class,
|
||||||
|
int.class,
|
||||||
|
Set.class,
|
||||||
|
int.class,
|
||||||
|
int.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long invokeCornerBounds(IrisComplex complex, Method method, IrisInterpolator interpolator, int x, int z) throws Exception {
|
||||||
|
Field gridBoundsCache = IrisComplex.class.getDeclaredField("gridBoundsCache");
|
||||||
|
gridBoundsCache.setAccessible(true);
|
||||||
|
ThreadLocal<?> cache = (ThreadLocal<?>) gridBoundsCache.get(complex);
|
||||||
|
return (long) method.invoke(complex, cache.get(), null, interpolator, 0, Set.<IrisGenerator>of(), x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float unpackLow(long packed) {
|
||||||
|
return Float.intBitsToFloat((int) (packed >>> 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
private float unpackHigh(long packed) {
|
||||||
|
return Float.intBitsToFloat((int) packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CountingInterpolator extends IrisInterpolator {
|
||||||
|
private final NoiseBounds bounds;
|
||||||
|
private final AtomicInteger invocations = new AtomicInteger();
|
||||||
|
|
||||||
|
private CountingInterpolator(double low, double high) {
|
||||||
|
bounds = new NoiseBounds(low, high);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||||
|
invocations.incrementAndGet();
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getInvocations() {
|
||||||
|
return invocations.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package art.arcane.iris.engine.actuator;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||||
|
import art.arcane.iris.engine.framework.SeedManager;
|
||||||
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformBiome;
|
||||||
|
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
|
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||||
|
import art.arcane.iris.util.project.hunk.Hunk;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisBiomeActuatorCoordinateTest {
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void derivativeScatterUsesEachWorldColumnCoordinate() {
|
||||||
|
bindPlatform();
|
||||||
|
Engine engine = engine();
|
||||||
|
IrisBiomeActuator actuator = new IrisBiomeActuator(engine);
|
||||||
|
Hunk<PlatformBiome> output = mock(Hunk.class);
|
||||||
|
when(output.getWidth()).thenReturn(2);
|
||||||
|
when(output.getDepth()).thenReturn(2);
|
||||||
|
when(output.getHeight()).thenReturn(1);
|
||||||
|
|
||||||
|
List<String> samples = new ArrayList<>();
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
when(biome.getSkyBiomeKey(any(RNG.class), same(engine), anyDouble(), anyDouble(), anyDouble()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
double worldX = invocation.getArgument(2);
|
||||||
|
double worldZ = invocation.getArgument(4);
|
||||||
|
samples.add((int) worldX + "," + (int) worldZ);
|
||||||
|
return "minecraft:plains";
|
||||||
|
});
|
||||||
|
|
||||||
|
ChunkedDataCache<IrisBiome> biomeCache = mock(ChunkedDataCache.class);
|
||||||
|
when(biomeCache.get(anyInt(), anyInt())).thenReturn(biome);
|
||||||
|
ChunkContext context = mock(ChunkContext.class);
|
||||||
|
when(context.getBiome()).thenReturn(biomeCache);
|
||||||
|
|
||||||
|
actuator.onActuate(100, -200, output, false, context);
|
||||||
|
|
||||||
|
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Engine engine() {
|
||||||
|
Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||||
|
SeedManager seedManager = mock(SeedManager.class);
|
||||||
|
when(seedManager.getBiome()).thenReturn(1337L);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getCacheID()).thenReturn(1);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seedManager);
|
||||||
|
when(engine.getMantle()).thenReturn(engineMantle);
|
||||||
|
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformBiome biome = mock(PlatformBiome.class);
|
||||||
|
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
when(registries.biome(anyString())).thenReturn(biome);
|
||||||
|
when(registries.block(anyString())).thenReturn(block);
|
||||||
|
PlatformBiomeWriter biomeWriter = mock(PlatformBiomeWriter.class);
|
||||||
|
when(biomeWriter.biomeIdFor(anyString())).thenReturn(1);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
when(platform.biomeWriter()).thenReturn(biomeWriter);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package art.arcane.iris.engine.decorator;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.SeedManager;
|
||||||
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||||
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.util.project.hunk.Hunk;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class IrisDecoratorCaveContextTest {
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void explicitCaveContextSkipsFluidWithoutMutatingBiome() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
SeedManager seedManager = mock(SeedManager.class);
|
||||||
|
doReturn(seedManager).when(engine).getSeedManager();
|
||||||
|
doReturn(mock(IrisData.class)).when(engine).getData();
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
doReturn(63).when(dimension).getFluidHeight();
|
||||||
|
doReturn(dimension).when(engine).getDimension();
|
||||||
|
|
||||||
|
IrisDecorator decorator = mock(IrisDecorator.class);
|
||||||
|
doReturn(true).when(decorator).passesChanceGate(any(), anyDouble(), anyDouble(), any());
|
||||||
|
doReturn(true).when(decorator).isStacking();
|
||||||
|
doReturn(true).when(decorator).isForcePlace();
|
||||||
|
doReturn(1).when(decorator).getHeight(any(), anyDouble(), anyDouble(), any());
|
||||||
|
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
doReturn(InferredType.LAND).when(biome).getInferredType();
|
||||||
|
doReturn(new IrisDecorator[]{decorator}).when(biome).getDecoratorBucket(IrisDecorationPart.NONE);
|
||||||
|
doReturn(new IrisDecorator[]{decorator}).when(biome).getDecoratorBucket(IrisDecorationPart.CEILING);
|
||||||
|
|
||||||
|
PlatformBlockState fluid = mock(PlatformBlockState.class);
|
||||||
|
doReturn(true).when(fluid).isFluid();
|
||||||
|
Hunk<PlatformBlockState> output = mock(Hunk.class);
|
||||||
|
doReturn(128).when(output).getHeight();
|
||||||
|
doReturn(fluid).when(output).get(0, 10, 0);
|
||||||
|
|
||||||
|
IrisSurfaceDecorator surfaceDecorator = new IrisSurfaceDecorator(engine);
|
||||||
|
surfaceDecorator.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, InferredType.CAVE, 10, 10);
|
||||||
|
DecoratorCore.PlaceOpts surfaceOptions = DecoratorCore.SCRATCH_OPTS.get();
|
||||||
|
assertTrue(surfaceOptions.caveSkipFluid);
|
||||||
|
assertFalse(surfaceOptions.underwater);
|
||||||
|
|
||||||
|
IrisCeilingDecorator ceilingDecorator = new IrisCeilingDecorator(engine);
|
||||||
|
ceilingDecorator.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, InferredType.CAVE, 10, 10);
|
||||||
|
DecoratorCore.PlaceOpts ceilingOptions = DecoratorCore.SCRATCH_OPTS.get();
|
||||||
|
assertTrue(ceilingOptions.caveSkipFluid);
|
||||||
|
assertEquals(InferredType.LAND, biome.getInferredType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nullInferenceRemainsSafeForNormalSurfaceContext() {
|
||||||
|
assertTrue(IrisSurfaceDecorator.isUnderwater(null, 10, 63));
|
||||||
|
assertFalse(IrisSurfaceDecorator.skipsFluid(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class StructurePlacementMarkerTest {
|
||||||
|
@Test
|
||||||
|
public void structureMarkerRoundTripsDelimiterSafeKeys() {
|
||||||
|
String objectKey = "pieces/room@east:alpha/樹";
|
||||||
|
String structureKey = "structures/village@night:beta/城";
|
||||||
|
|
||||||
|
String encoded = StructurePlacementMarker.encodeStructure(objectKey, 918273, structureKey);
|
||||||
|
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(encoded);
|
||||||
|
|
||||||
|
assertTrue(encoded.startsWith("@iris-structure:v1:"));
|
||||||
|
assertEquals(objectKey, decoded.objectKey());
|
||||||
|
assertEquals(918273, decoded.placementId());
|
||||||
|
assertEquals(structureKey, decoded.structureKey());
|
||||||
|
assertTrue(decoded.structureAware());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void legacyMarkerDecodesWithoutChangingFields() {
|
||||||
|
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode("objects/oak_tree@42");
|
||||||
|
|
||||||
|
assertEquals("objects/oak_tree", decoded.objectKey());
|
||||||
|
assertEquals(42, decoded.placementId());
|
||||||
|
assertNull(decoded.structureKey());
|
||||||
|
assertFalse(decoded.structureAware());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedAndUnknownMarkersFailSafely() {
|
||||||
|
assertNull(StructurePlacementMarker.decode(null));
|
||||||
|
assertNull(StructurePlacementMarker.decode(""));
|
||||||
|
assertNull(StructurePlacementMarker.decode("objects/oak_tree"));
|
||||||
|
assertNull(StructurePlacementMarker.decode("objects/oak_tree@not-an-id"));
|
||||||
|
assertNull(StructurePlacementMarker.decode("objects/oak_tree@42@extra"));
|
||||||
|
assertNull(StructurePlacementMarker.decode("@iris-structure:v2:b2JqZWN0:42:c3RydWN0dXJl"));
|
||||||
|
assertNull(StructurePlacementMarker.decode("@iris-structure:v1:not%base64:42:c3RydWN0dXJl"));
|
||||||
|
assertNull(StructurePlacementMarker.decode("@iris-structure:v1:b2JqZWN0:42:"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -37,9 +37,11 @@ import java.util.HashSet;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -117,6 +119,43 @@ public class IrisCaveCarver3DNearParityTest {
|
|||||||
assertEquals(firstCapture.carvedLiquids, secondCapture.carvedLiquids);
|
assertEquals(firstCapture.carvedLiquids, secondCapture.carvedLiquids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void warpCacheDoesNotCrossContaminateCarverInstancesOnSameThread() throws Exception {
|
||||||
|
Engine engine = createEngine(128, 92);
|
||||||
|
IrisCaveProfile firstProfile = createProfile(true, false);
|
||||||
|
firstProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.12D));
|
||||||
|
IrisCaveProfile secondProfile = createProfile(true, false);
|
||||||
|
secondProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
|
||||||
|
IrisCaveProfile controlProfile = createProfile(true, false);
|
||||||
|
controlProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
|
||||||
|
IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, firstProfile);
|
||||||
|
IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, secondProfile);
|
||||||
|
IrisCaveCarver3D controlCarver = new IrisCaveCarver3D(engine, controlProfile);
|
||||||
|
int x = 48;
|
||||||
|
int y = 64;
|
||||||
|
int z = -352;
|
||||||
|
|
||||||
|
double firstDensity = sampleDensity(firstCarver, x, y, z);
|
||||||
|
double secondDensity = sampleDensity(secondCarver, x, y, z);
|
||||||
|
AtomicReference<Double> controlDensity = new AtomicReference<>();
|
||||||
|
AtomicReference<Throwable> controlFailure = new AtomicReference<>();
|
||||||
|
Thread controlThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
controlDensity.set(sampleDensity(controlCarver, x, y, z));
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
controlFailure.set(throwable);
|
||||||
|
}
|
||||||
|
}, "Iris cave warp cache control");
|
||||||
|
controlThread.start();
|
||||||
|
controlThread.join();
|
||||||
|
|
||||||
|
if (controlFailure.get() != null) {
|
||||||
|
throw new AssertionError(controlFailure.get());
|
||||||
|
}
|
||||||
|
assertNotEquals(firstDensity, controlDensity.get(), 0D);
|
||||||
|
assertEquals(controlDensity.get(), secondDensity, 0D);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void exactPathCarvesChunkEdgesAndRespectsWorldHeightClipping() {
|
public void exactPathCarvesChunkEdgesAndRespectsWorldHeightClipping() {
|
||||||
Engine engine = createEngine(48, 46);
|
Engine engine = createEngine(48, 46);
|
||||||
@@ -277,6 +316,10 @@ public class IrisCaveCarver3DNearParityTest {
|
|||||||
return elapsed;
|
return elapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double sampleDensity(IrisCaveCarver3D carver, int x, int y, int z) throws Exception {
|
||||||
|
return (double) sampleDensityMethod.invoke(carver, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
private long runNaiveOnce(IrisCaveCarver3D carver, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights, int worldHeight) throws Exception {
|
private long runNaiveOnce(IrisCaveCarver3D carver, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights, int worldHeight) throws Exception {
|
||||||
WriterCapture capture = createWriterCapture(worldHeight);
|
WriterCapture capture = createWriterCapture(worldHeight);
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisStructureComponentMarkerTest {
|
||||||
|
@Test
|
||||||
|
public void markerFilterAcceptsOnlyStorageContainers() {
|
||||||
|
PlatformBlockState storage = mock(PlatformBlockState.class);
|
||||||
|
PlatformBlockState solid = mock(PlatformBlockState.class);
|
||||||
|
when(storage.isStorageChest()).thenReturn(true);
|
||||||
|
when(solid.isStorageChest()).thenReturn(false);
|
||||||
|
|
||||||
|
assertTrue(IrisStructureComponent.shouldWriteStructureMarker(storage));
|
||||||
|
assertFalse(IrisStructureComponent.shouldWriteStructureMarker(solid));
|
||||||
|
assertFalse(IrisStructureComponent.shouldWriteStructureMarker(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void placementIdIsStableAndCoordinateSensitive() {
|
||||||
|
int first = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 20, -14, 35);
|
||||||
|
int repeated = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 20, -14, 35);
|
||||||
|
int moved = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 21, -14, 35);
|
||||||
|
|
||||||
|
assertEquals(first, repeated);
|
||||||
|
assertNotEquals(first, moved);
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.nms.container.Pair;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.mantle.MantleComponent;
|
||||||
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.engine.mantle.MatterGenerator;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
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.util.project.context.ChunkContext;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class MantleCarvingComponentBoundaryRadiusTest {
|
||||||
|
@Before
|
||||||
|
public void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
when(registries.block(anyString())).thenReturn(block);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.registries()).thenReturn(registries);
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boundaryWallResolutionSchedulesAdjacentCarvingChunks() {
|
||||||
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
MantleCarvingComponent component = new MantleCarvingComponent(engineMantle);
|
||||||
|
|
||||||
|
assertEquals(1, component.getRadius());
|
||||||
|
assertEquals(1, Math.ceilDiv(component.getRadius(), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void matterGenerationRunsCarvingForAllAdjacentChunks() {
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
when(dimension.isUseMantle()).thenReturn(true);
|
||||||
|
Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
|
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||||
|
when(chunk.use()).thenReturn(chunk);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
Runnable task = invocation.getArgument(1);
|
||||||
|
task.run();
|
||||||
|
return null;
|
||||||
|
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
|
||||||
|
|
||||||
|
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(engine.getMantle()).thenReturn(engineMantle);
|
||||||
|
when(engineMantle.getEngine()).thenReturn(engine);
|
||||||
|
|
||||||
|
List<String> generatedChunks = new ArrayList<>();
|
||||||
|
MantleCarvingComponent component = spy(new MantleCarvingComponent(engineMantle));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
int chunkX = invocation.getArgument(1);
|
||||||
|
int chunkZ = invocation.getArgument(2);
|
||||||
|
generatedChunks.add(chunkX + "," + chunkZ);
|
||||||
|
return null;
|
||||||
|
}).when(component).generateLayer(any(), anyInt(), anyInt(), any());
|
||||||
|
|
||||||
|
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, component);
|
||||||
|
generator.generateMatter(4, -7, false, mock(ChunkContext.class));
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
"3,-8", "3,-7", "3,-6",
|
||||||
|
"4,-8", "4,-7", "4,-6",
|
||||||
|
"5,-8", "5,-7", "5,-6"
|
||||||
|
), generatedChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestMatterGenerator implements MatterGenerator {
|
||||||
|
private final Engine engine;
|
||||||
|
private final Mantle<Matter> mantle;
|
||||||
|
private final List<Pair<List<MantleComponent>, Integer>> components;
|
||||||
|
|
||||||
|
private TestMatterGenerator(Engine engine, Mantle<Matter> mantle, MantleComponent component) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.mantle = mantle;
|
||||||
|
this.components = List.of(new Pair<>(List.of(component), 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine getEngine() {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mantle<Matter> getMantle() {
|
||||||
|
return mantle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRadius() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRealRadius() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Pair<List<MantleComponent>, Integer>> getComponents() {
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package art.arcane.iris.engine.modifier;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.object.InferredType;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
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.util.project.hunk.Hunk;
|
||||||
|
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class IrisCarveModifierInferenceIsolationTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||||
|
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||||
|
doReturn(block).when(registries).block(anyString());
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
doReturn(registries).when(platform).registries();
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void unbindPlatform() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void cavePaintingDoesNotMutateSharedBiomeInference() throws Exception {
|
||||||
|
IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND);
|
||||||
|
biome.getLayers().clear();
|
||||||
|
biome.getCaveCeilingLayers().clear();
|
||||||
|
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
doReturn(mock(IrisData.class)).when(engine).getData();
|
||||||
|
doReturn(mock(IrisDimension.class)).when(engine).getDimension();
|
||||||
|
doReturn(mock(IrisComplex.class)).when(engine).getComplex();
|
||||||
|
doReturn(IrisWorld.builder().minHeight(-256).maxHeight(512).build()).when(engine).getWorld();
|
||||||
|
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
|
||||||
|
doReturn(engine).when(modifier).getEngine();
|
||||||
|
|
||||||
|
Map<String, IrisBiome> customBiomes = new HashMap<>();
|
||||||
|
customBiomes.put("shared", biome);
|
||||||
|
Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
|
||||||
|
"paintBoundaryZone",
|
||||||
|
Hunk.class,
|
||||||
|
MatterCavern.class,
|
||||||
|
int.class,
|
||||||
|
int.class,
|
||||||
|
int.class,
|
||||||
|
int.class,
|
||||||
|
int.class,
|
||||||
|
int.class,
|
||||||
|
IrisDimensionCarvingResolver.State.class,
|
||||||
|
Long2ObjectOpenHashMap.class,
|
||||||
|
Map.class
|
||||||
|
);
|
||||||
|
paintBoundaryZone.setAccessible(true);
|
||||||
|
paintBoundaryZone.invoke(
|
||||||
|
modifier,
|
||||||
|
mock(Hunk.class),
|
||||||
|
new MatterCavern(true, "shared", (byte) 0),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
new IrisDimensionCarvingResolver.State(),
|
||||||
|
new Long2ObjectOpenHashMap<IrisBiome>(),
|
||||||
|
customBiomes
|
||||||
|
);
|
||||||
|
|
||||||
|
IrisBiome fallback = new IrisBiome();
|
||||||
|
ProceduralStream<IrisBiome> landStream = mock(ProceduralStream.class);
|
||||||
|
doReturn(fallback).when(landStream).get(anyDouble(), anyDouble());
|
||||||
|
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||||
|
Field landBiomeStream = IrisComplex.class.getDeclaredField("landBiomeStream");
|
||||||
|
landBiomeStream.setAccessible(true);
|
||||||
|
landBiomeStream.set(complex, landStream);
|
||||||
|
IrisRegion region = mock(IrisRegion.class);
|
||||||
|
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
|
||||||
|
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
|
||||||
|
"fixBiomeType",
|
||||||
|
Double.class,
|
||||||
|
IrisBiome.class,
|
||||||
|
IrisRegion.class,
|
||||||
|
Double.class,
|
||||||
|
Double.class,
|
||||||
|
double.class
|
||||||
|
);
|
||||||
|
fixBiomeType.setAccessible(true);
|
||||||
|
IrisBiome resolved = (IrisBiome) fixBiomeType.invoke(complex, 10D, biome, region, 0D, 0D, 0D);
|
||||||
|
assertSame(biome, resolved);
|
||||||
|
assertEquals(InferredType.LAND, biome.getInferredType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
public class IrisBiomeCustomSpawnTest {
|
||||||
|
@Test
|
||||||
|
public void normalizesNamespacedEntityKey() {
|
||||||
|
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType(" Minecraft:Slime ");
|
||||||
|
|
||||||
|
assertEquals("minecraft:slime", spawn.getTypeKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prefixesBareEntityKey() {
|
||||||
|
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType("Slime");
|
||||||
|
|
||||||
|
assertEquals("minecraft:slime", spawn.getTypeKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void returnsNullForBlankEntityKey() {
|
||||||
|
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType(" ");
|
||||||
|
|
||||||
|
assertNull(spawn.getTypeKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.SeedManager;
|
||||||
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
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.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisBiomeGeneratorSeedIsolationTest {
|
||||||
|
private static final double SAMPLE_X = 128D;
|
||||||
|
private static final double SAMPLE_Z = -64D;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void engineSeedMakesPaperStyleFirstCallOrderIrrelevant() {
|
||||||
|
IrisData data = dataWithActiveEngine(new AtomicReference<>(engine(1337L)));
|
||||||
|
IrisBiome paperFirst = biome(data);
|
||||||
|
IrisBiome moddedFirst = biome(data);
|
||||||
|
|
||||||
|
CNG paperGenerator = paperFirst.getBiomeGenerator(new RNG(8457289L));
|
||||||
|
CNG moddedGenerator = moddedFirst.getBiomeGenerator(new RNG(-991245L));
|
||||||
|
|
||||||
|
assertEquals(paperGenerator.noise(SAMPLE_X, SAMPLE_Z), moddedGenerator.noise(SAMPLE_X, SAMPLE_Z), 0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distinctEnginesKeepDistinctBiomeGenerators() {
|
||||||
|
Engine firstEngine = engine(1337L);
|
||||||
|
Engine secondEngine = engine(7331L);
|
||||||
|
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
|
||||||
|
|
||||||
|
CNG first = biome.getBiomeGenerator(new RNG(1L), firstEngine);
|
||||||
|
CNG second = biome.getBiomeGenerator(new RNG(1L), secondEngine);
|
||||||
|
|
||||||
|
assertNotSame(first, second);
|
||||||
|
assertNotEquals(first.noise(SAMPLE_X, SAMPLE_Z), second.noise(SAMPLE_X, SAMPLE_Z), 0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sameSeedEnginesSharingBiomeReuseBiomeGenerator() {
|
||||||
|
Engine firstEngine = engine(1337L);
|
||||||
|
Engine secondEngine = engine(1337L);
|
||||||
|
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
|
||||||
|
|
||||||
|
CNG first = biome.getBiomeGenerator(new RNG(11L), firstEngine);
|
||||||
|
CNG second = biome.getBiomeGenerator(new RNG(99L), secondEngine);
|
||||||
|
|
||||||
|
assertSame(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void engineLessToolingUsesSuppliedRngSeed() {
|
||||||
|
IrisBiome biome = biome(null);
|
||||||
|
|
||||||
|
CNG first = biome.getBiomeGenerator(new RNG(11L));
|
||||||
|
CNG sameSeed = biome.getBiomeGenerator(new RNG(11L));
|
||||||
|
CNG second = biome.getBiomeGenerator(new RNG(99L));
|
||||||
|
|
||||||
|
assertSame(first, sameSeed);
|
||||||
|
assertNotSame(first, second);
|
||||||
|
assertNotEquals(first.noise(SAMPLE_X, SAMPLE_Z), second.noise(SAMPLE_X, SAMPLE_Z), 0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentExactEnginesIgnoreLoaderEngine() throws Exception {
|
||||||
|
Engine firstEngine = engine(1337L);
|
||||||
|
Engine secondEngine = engine(7331L);
|
||||||
|
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
try {
|
||||||
|
Future<CNG> firstFuture = executor.submit(() -> repeatedlyResolve(biome, firstEngine, start));
|
||||||
|
Future<CNG> secondFuture = executor.submit(() -> repeatedlyResolve(biome, secondEngine, start));
|
||||||
|
start.countDown();
|
||||||
|
|
||||||
|
CNG first = firstFuture.get();
|
||||||
|
CNG second = secondFuture.get();
|
||||||
|
assertNotSame(first, second);
|
||||||
|
assertSame(first, biome.getBiomeGenerator(new RNG(99L), firstEngine));
|
||||||
|
assertSame(second, biome.getBiomeGenerator(new RNG(99L), secondEngine));
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void biomeGeneratorCacheEvictsOldSeeds() {
|
||||||
|
IrisBiome biome = biome(null);
|
||||||
|
CNG first = biome.getBiomeGenerator(new RNG(0L));
|
||||||
|
|
||||||
|
for (long seed = 1L; seed <= 8L; seed++) {
|
||||||
|
biome.getBiomeGenerator(new RNG(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNotSame(first, biome.getBiomeGenerator(new RNG(0L)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBiome biome(IrisData data) {
|
||||||
|
IrisBiome biome = new IrisBiome().setName("Scatter Test");
|
||||||
|
biome.setLoader(data);
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisData dataWithActiveEngine(AtomicReference<Engine> activeEngine) {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
when(data.getEngine()).thenAnswer(ignored -> activeEngine.get());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Engine engine(long biomeSeed) {
|
||||||
|
SeedManager seedManager = mock(SeedManager.class);
|
||||||
|
when(seedManager.getBiome()).thenReturn(biomeSeed);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getSeedManager()).thenReturn(seedManager);
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CNG repeatedlyResolve(IrisBiome biome, Engine engine, CountDownLatch start) throws InterruptedException {
|
||||||
|
start.await();
|
||||||
|
CNG generator = biome.getBiomeGenerator(new RNG(1L), engine);
|
||||||
|
for (int index = 0; index < 100; index++) {
|
||||||
|
assertSame(generator, biome.getBiomeGenerator(new RNG(index + 2L), engine));
|
||||||
|
}
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.json.JSONArray;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
|
public class IrisDimensionBiomeTagTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void biomeTagWritesAreSortedAndDeduplicated() throws Exception {
|
||||||
|
Path output = temporaryFolder.getRoot().toPath().resolve("allows_surface_slime_spawns.json");
|
||||||
|
|
||||||
|
IrisDimension.writeBiomeTag(output, "overworld:swamp_b");
|
||||||
|
IrisDimension.writeBiomeTag(output, "overworld:swamp_a");
|
||||||
|
IrisDimension.writeBiomeTag(output, "overworld:swamp_b");
|
||||||
|
|
||||||
|
JSONObject tag = new JSONObject(Files.readString(output, StandardCharsets.UTF_8));
|
||||||
|
JSONArray values = tag.getJSONArray("values");
|
||||||
|
assertFalse(tag.getBoolean("replace"));
|
||||||
|
assertEquals(2, values.length());
|
||||||
|
assertEquals("overworld:swamp_a", values.getString(0));
|
||||||
|
assertEquals("overworld:swamp_b", values.getString(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void customBiomeTagUsesTheMinecraftBiomeTagPath() throws Exception {
|
||||||
|
KList<String> tags = new KList<>();
|
||||||
|
tags.add("minecraft:allows_surface_slime_spawns");
|
||||||
|
IrisDimension.installBiomeTags(temporaryFolder.getRoot(), "overworld:swamp", tags);
|
||||||
|
|
||||||
|
Path output = temporaryFolder.getRoot().toPath()
|
||||||
|
.resolve("iris/data/minecraft/tags/worldgen/biome/allows_surface_slime_spawns.json");
|
||||||
|
JSONObject tag = new JSONObject(Files.readString(output, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
assertEquals("overworld:swamp", tag.getJSONArray("values").getString(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisStructureLootPlacementTest {
|
||||||
|
@Test
|
||||||
|
public void createsPiecePlacementWithAuthoredLootOrderAndDefaultWeights() {
|
||||||
|
IrisStructure structure = new IrisStructure();
|
||||||
|
structure.getLoot().add("village_common");
|
||||||
|
structure.getLoot().add("village_rare");
|
||||||
|
|
||||||
|
IrisObjectPlacement placement = structure.createLootPlacement("pieces/village_house");
|
||||||
|
|
||||||
|
assertEquals(1, placement.getPlace().size());
|
||||||
|
assertEquals("pieces/village_house", placement.getPlace().get(0));
|
||||||
|
assertEquals(2, placement.getLoot().size());
|
||||||
|
assertEquals("village_common", placement.getLoot().get(0).getName());
|
||||||
|
assertEquals(1, placement.getLoot().get(0).getWeight());
|
||||||
|
assertTrue(placement.getLoot().get(0).getFilter().isEmpty());
|
||||||
|
assertEquals("village_rare", placement.getLoot().get(1).getName());
|
||||||
|
assertEquals(1, placement.getLoot().get(1).getWeight());
|
||||||
|
assertTrue(placement.getLoot().get(1).getFilter().isEmpty());
|
||||||
|
assertFalse(placement.isOverrideGlobalLoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createsEmptyLootPlacementWhenStructureHasNoLoot() {
|
||||||
|
IrisObjectPlacement placement = new IrisStructure().createLootPlacement("pieces/empty_house");
|
||||||
|
|
||||||
|
assertEquals("pieces/empty_house", placement.getPlace().get(0));
|
||||||
|
assertTrue(placement.getLoot().isEmpty());
|
||||||
|
assertFalse(placement.isOverrideGlobalLoot());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name: ${name}
|
|
||||||
version: ${version}
|
|
||||||
main: ${main}
|
|
||||||
load: STARTUP
|
|
||||||
authors: [ cyberpwn, NextdoorPsycho ]
|
|
||||||
website: volmit.com
|
|
||||||
description: More than a Dimension!
|
|
||||||
libraries:
|
|
||||||
- org.zeroturnaround:zt-zip:1.14
|
|
||||||
- com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2
|
|
||||||
- org.ow2.asm:asm:9.2
|
|
||||||
- com.google.code.gson:gson:2.8.7
|
|
||||||
- it.unimi.dsi:fastutil:8.5.4
|
|
||||||
- com.google.guava:guava:30.1.1-jre
|
|
||||||
- bsf:bsf:2.4.0
|
|
||||||
- rhino:js:1.7R2
|
|
||||||
commands:
|
|
||||||
iris:
|
|
||||||
aliases: [ ir, irs ]
|
|
||||||
api-version: ${apiversion}
|
|
||||||
hotload-dependencies: false
|
|
||||||
@@ -16,6 +16,7 @@ String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':core'))
|
implementation(project(':core'))
|
||||||
compileOnly(libs.spigot)
|
compileOnly(libs.spigot)
|
||||||
|
testImplementation('junit:junit:4.13.2')
|
||||||
compileOnly(volmLibCoordinate) {
|
compileOnly(volmLibCoordinate) {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
@@ -44,6 +45,10 @@ tasks.named('run', JavaExec).configure {
|
|||||||
dependsOn(':core:compileJava')
|
dependsOn(':core:compileJava')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('test').configure {
|
||||||
|
dependsOn(':core:compileJava')
|
||||||
|
}
|
||||||
|
|
||||||
String probePack = providers.gradleProperty('probePack')
|
String probePack = providers.gradleProperty('probePack')
|
||||||
.orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.2/plugins/Iris/packs/overworld')
|
.orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.2/plugins/Iris/packs/overworld')
|
||||||
.get()
|
.get()
|
||||||
|
|||||||
@@ -18,15 +18,23 @@
|
|||||||
|
|
||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class ClassloadProbe {
|
public final class ClassloadProbe {
|
||||||
|
private static final String ALLOWLIST_RESOURCE = "/classload-allowlist.tsv";
|
||||||
|
|
||||||
private static final String[] CRITICAL_PREFIXES = {
|
private static final String[] CRITICAL_PREFIXES = {
|
||||||
"art.arcane.iris.engine.",
|
"art.arcane.iris.engine.",
|
||||||
"art.arcane.iris.util.",
|
"art.arcane.iris.util.",
|
||||||
@@ -34,26 +42,6 @@ public final class ClassloadProbe {
|
|||||||
"art.arcane.iris.spi.",
|
"art.arcane.iris.spi.",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String[] EXEMPT_PREFIXES = {
|
|
||||||
"art.arcane.iris.platform.bukkit.",
|
|
||||||
"art.arcane.iris.engine.platform.",
|
|
||||||
"art.arcane.iris.util.common.plugin.",
|
|
||||||
"art.arcane.iris.util.common.inventorygui.",
|
|
||||||
"art.arcane.iris.util.common.director.",
|
|
||||||
"art.arcane.iris.util.common.data.registry.",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String[] EXEMPT_CLASSES = {
|
|
||||||
"art.arcane.iris.util.common.misc.Bindings",
|
|
||||||
"art.arcane.iris.util.common.misc.SlimJar",
|
|
||||||
"art.arcane.iris.util.common.misc.ServerProperties",
|
|
||||||
"art.arcane.iris.util.common.data.IrisCustomData",
|
|
||||||
"art.arcane.iris.engine.IrisWorldManager",
|
|
||||||
"art.arcane.iris.engine.framework.EngineAssignedWorldManager",
|
|
||||||
"art.arcane.iris.engine.object.StudioMode",
|
|
||||||
"art.arcane.iris.engine.framework.placer.WorldObjectPlacer",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
art.arcane.iris.spi.IrisPlatforms.bind(new StubPlatform());
|
art.arcane.iris.spi.IrisPlatforms.bind(new StubPlatform());
|
||||||
boolean bukkitPresent = true;
|
boolean bukkitPresent = true;
|
||||||
@@ -63,39 +51,42 @@ public final class ClassloadProbe {
|
|||||||
bukkitPresent = false;
|
bukkitPresent = false;
|
||||||
}
|
}
|
||||||
System.out.println("[probe] org.bukkit on classpath: " + bukkitPresent);
|
System.out.println("[probe] org.bukkit on classpath: " + bukkitPresent);
|
||||||
|
if (bukkitPresent) {
|
||||||
Path classesRoot = Path.of(args[0]);
|
System.out.println("[probe] RESULT: FAIL - org.bukkit must be absent from the classload probe runtime");
|
||||||
List<String> names = new ArrayList<>();
|
System.exit(1);
|
||||||
try (Stream<Path> walk = Files.walk(classesRoot)) {
|
return;
|
||||||
walk.filter(p -> p.toString().endsWith(".class"))
|
|
||||||
.forEach(p -> {
|
|
||||||
String rel = classesRoot.relativize(p).toString();
|
|
||||||
String name = rel.substring(0, rel.length() - 6).replace('/', '.');
|
|
||||||
if (name.contains("$")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
names.add(name);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
names.sort(String::compareTo);
|
|
||||||
|
|
||||||
TreeMap<String, String> criticalFailures = new TreeMap<>();
|
TreeMap<String, Allowance> allowlist = loadAllowlist();
|
||||||
TreeMap<String, String> otherFailures = new TreeMap<>();
|
Path classesRoot = Path.of(args[0]);
|
||||||
|
List<String> names = scanClassNames(classesRoot);
|
||||||
|
|
||||||
|
TreeMap<String, Failure> criticalFailures = new TreeMap<>();
|
||||||
|
TreeMap<String, Failure> otherFailures = new TreeMap<>();
|
||||||
int loaded = 0;
|
int loaded = 0;
|
||||||
|
int nestedTotal = 0;
|
||||||
|
int nestedLoaded = 0;
|
||||||
int criticalTotal = 0;
|
int criticalTotal = 0;
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
boolean critical = matchesAny(name, CRITICAL_PREFIXES) && !matchesAny(name, EXEMPT_PREFIXES) && !exactAny(name);
|
boolean nested = name.contains("$");
|
||||||
|
if (nested) {
|
||||||
|
nestedTotal++;
|
||||||
|
}
|
||||||
|
boolean critical = matchesAny(name, CRITICAL_PREFIXES) && !allowlist.containsKey(name);
|
||||||
if (critical) {
|
if (critical) {
|
||||||
criticalTotal++;
|
criticalTotal++;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Class.forName(name, true, ClassloadProbe.class.getClassLoader());
|
Class.forName(name, true, ClassloadProbe.class.getClassLoader());
|
||||||
loaded++;
|
loaded++;
|
||||||
|
if (nested) {
|
||||||
|
nestedLoaded++;
|
||||||
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
if (System.getenv("PROBE_TRACE") != null && name.contains(System.getenv("PROBE_TRACE"))) {
|
if (System.getenv("PROBE_TRACE") != null && name.contains(System.getenv("PROBE_TRACE"))) {
|
||||||
failure.printStackTrace(System.out);
|
failure.printStackTrace(System.out);
|
||||||
}
|
}
|
||||||
String cause = rootCause(failure);
|
Failure cause = Failure.from(failure);
|
||||||
if (critical) {
|
if (critical) {
|
||||||
criticalFailures.put(name, cause);
|
criticalFailures.put(name, cause);
|
||||||
} else {
|
} else {
|
||||||
@@ -105,25 +96,56 @@ public final class ClassloadProbe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("[probe] classes scanned: " + names.size() + ", initialized OK: " + loaded);
|
System.out.println("[probe] classes scanned: " + names.size() + ", initialized OK: " + loaded);
|
||||||
|
System.out.println("[probe] nested classes scanned: " + nestedTotal + ", initialized OK: " + nestedLoaded
|
||||||
|
+ ", initialization failures: " + (nestedTotal - nestedLoaded));
|
||||||
System.out.println("[probe] critical set: " + criticalTotal + ", critical failures: " + criticalFailures.size());
|
System.out.println("[probe] critical set: " + criticalTotal + ", critical failures: " + criticalFailures.size());
|
||||||
criticalFailures.forEach((name, cause) -> System.out.println(" CRITICAL " + name + " -> " + cause));
|
criticalFailures.forEach((name, cause) -> System.out.println(" CRITICAL " + name + " -> " + cause.display()));
|
||||||
System.out.println("[probe] non-critical failures: " + otherFailures.size());
|
System.out.println("[probe] non-critical failures: " + otherFailures.size());
|
||||||
otherFailures.forEach((name, cause) -> System.out.println(" other " + name + " -> " + cause));
|
Review review = review(otherFailures, allowlist);
|
||||||
|
System.out.println("[probe] reviewed allowances: " + allowlist.size() + ", matched failures: " + review.matched());
|
||||||
|
otherFailures.forEach((name, cause) -> {
|
||||||
|
Allowance allowance = allowlist.get(name);
|
||||||
|
String status = allowance != null && allowance.matches(cause) ? "allowed " + allowance.category() : "UNEXPECTED";
|
||||||
|
System.out.println(" " + status + " " + name + " -> " + cause.display());
|
||||||
|
});
|
||||||
|
System.out.println("[probe] unexpected non-critical failures: " + review.unexpected().size());
|
||||||
|
System.out.println("[probe] stale allowances: " + review.staleAllowances().size());
|
||||||
|
review.staleAllowances().forEach((name, allowance) -> System.out.println(" STALE " + name + " -> " + allowance));
|
||||||
|
|
||||||
if (!criticalFailures.isEmpty()) {
|
if (!criticalFailures.isEmpty() || !review.passes()) {
|
||||||
System.out.println("[probe] RESULT: FAIL");
|
System.out.println("[probe] RESULT: FAIL");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
System.out.println("[probe] RESULT: PASS");
|
System.out.println("[probe] RESULT: PASS");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean exactAny(String name) {
|
static Review review(Map<String, Failure> failures, Map<String, Allowance> allowlist) {
|
||||||
for (String exempt : EXEMPT_CLASSES) {
|
TreeMap<String, Failure> unexpected = new TreeMap<>();
|
||||||
if (name.equals(exempt)) {
|
TreeMap<String, Allowance> staleAllowances = new TreeMap<>(allowlist);
|
||||||
return true;
|
int matched = 0;
|
||||||
|
for (Map.Entry<String, Failure> entry : failures.entrySet()) {
|
||||||
|
Allowance allowance = allowlist.get(entry.getKey());
|
||||||
|
staleAllowances.remove(entry.getKey());
|
||||||
|
if (allowance == null || !allowance.matches(entry.getValue())) {
|
||||||
|
unexpected.put(entry.getKey(), entry.getValue());
|
||||||
|
} else {
|
||||||
|
matched++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return new Review(matched, unexpected, staleAllowances);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> scanClassNames(Path classesRoot) throws IOException {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
try (Stream<Path> walk = Files.walk(classesRoot)) {
|
||||||
|
walk.filter(path -> path.toString().endsWith(".class"))
|
||||||
|
.forEach(path -> {
|
||||||
|
String relative = classesRoot.relativize(path).toString();
|
||||||
|
names.add(relative.substring(0, relative.length() - 6).replace(File.separatorChar, '.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
names.sort(String::compareTo);
|
||||||
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean matchesAny(String name, String[] prefixes) {
|
private static boolean matchesAny(String name, String[] prefixes) {
|
||||||
@@ -135,11 +157,91 @@ public final class ClassloadProbe {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String rootCause(Throwable failure) {
|
private static TreeMap<String, Allowance> loadAllowlist() throws IOException {
|
||||||
Throwable cause = failure;
|
InputStream resource = ClassloadProbe.class.getResourceAsStream(ALLOWLIST_RESOURCE);
|
||||||
while (cause.getCause() != null && cause.getCause() != cause) {
|
if (resource == null) {
|
||||||
cause = cause.getCause();
|
throw new IOException("Classload allowlist resource not found: " + ALLOWLIST_RESOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeMap<String, Allowance> allowlist = new TreeMap<>();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
int lineNumber = 0;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
lineNumber++;
|
||||||
|
if (line.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] fields = line.split("\\t", -1);
|
||||||
|
if (fields.length != 3 || fields[0].isBlank() || fields[2].isBlank()) {
|
||||||
|
throw new IOException("Invalid classload allowlist entry at line " + lineNumber);
|
||||||
|
}
|
||||||
|
Allowance allowance;
|
||||||
|
try {
|
||||||
|
allowance = new Allowance(AllowanceCategory.valueOf(fields[1]), fields[2]);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid classload allowlist category at line " + lineNumber + ": " + fields[1], exception);
|
||||||
|
}
|
||||||
|
if (allowlist.put(fields[0], allowance) != null) {
|
||||||
|
throw new IOException("Duplicate classload allowlist class at line " + lineNumber + ": " + fields[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AllowanceCategory {
|
||||||
|
BUKKIT_API,
|
||||||
|
PAPERLIB_API,
|
||||||
|
ADVENTURE_API,
|
||||||
|
MYTHICMOBS_API,
|
||||||
|
SERVER_RUNTIME_FILE
|
||||||
|
}
|
||||||
|
|
||||||
|
record Allowance(AllowanceCategory category, String detail) {
|
||||||
|
boolean matches(Failure failure) {
|
||||||
|
return switch (category) {
|
||||||
|
case BUKKIT_API -> detail.startsWith("org.bukkit.") && failure.isMissingClassIn("org.bukkit.");
|
||||||
|
case PAPERLIB_API -> detail.startsWith("io.papermc.lib.") && failure.isMissingClassIn("io.papermc.lib.");
|
||||||
|
case ADVENTURE_API -> detail.startsWith("net.kyori.adventure.") && failure.isMissingClassIn("net.kyori.adventure.");
|
||||||
|
case MYTHICMOBS_API -> detail.startsWith("io.lumine.mythic.") && failure.isMissingClassIn("io.lumine.mythic.");
|
||||||
|
case SERVER_RUNTIME_FILE -> failure.exceptionClass().equals("java.io.FileNotFoundException")
|
||||||
|
&& failure.message().startsWith(detail);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Failure(String exceptionClass, String message) {
|
||||||
|
static Failure from(Throwable failure) {
|
||||||
|
Throwable cause = failure;
|
||||||
|
while (cause.getCause() != null && cause.getCause() != cause) {
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
return new Failure(cause.getClass().getName(), String.valueOf(cause.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isMissingClassIn(String packagePrefix) {
|
||||||
|
if (exceptionClass.equals("java.lang.ClassNotFoundException")) {
|
||||||
|
return message.startsWith(packagePrefix);
|
||||||
|
}
|
||||||
|
String internalPrefix = packagePrefix.replace('.', '/');
|
||||||
|
if (exceptionClass.equals("java.lang.NoClassDefFoundError")) {
|
||||||
|
return message.startsWith(internalPrefix);
|
||||||
|
}
|
||||||
|
return exceptionClass.equals("java.lang.ExceptionInInitializerError")
|
||||||
|
&& message.contains("NoClassDefFoundError: " + internalPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
String display() {
|
||||||
|
int separator = exceptionClass.lastIndexOf('.');
|
||||||
|
String simpleName = separator < 0 ? exceptionClass : exceptionClass.substring(separator + 1);
|
||||||
|
return simpleName + ": " + message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Review(int matched, TreeMap<String, Failure> unexpected, TreeMap<String, Allowance> staleAllowances) {
|
||||||
|
boolean passes() {
|
||||||
|
return unexpected.isEmpty() && staleAllowances.isEmpty();
|
||||||
}
|
}
|
||||||
return cause.getClass().getSimpleName() + ": " + cause.getMessage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
art.arcane.iris.core.IrisWorlds BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.core.ServerConfigurator BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.core.edit.BlockSignal BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.core.edit.BlockSignal$1 BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.core.edit.DustRevealer ADVENTURE_API net.kyori.adventure.text.format.TextColor
|
||||||
|
art.arcane.iris.core.events.IrisEngineEvent BUKKIT_API org.bukkit.event.Event
|
||||||
|
art.arcane.iris.core.events.IrisEngineHotloadEvent BUKKIT_API org.bukkit.event.Event
|
||||||
|
art.arcane.iris.core.events.IrisLootEvent BUKKIT_API org.bukkit.event.Event
|
||||||
|
art.arcane.iris.core.events.IrisLootEvent$1 BUKKIT_API org.bukkit.loot.LootTable
|
||||||
|
art.arcane.iris.core.events.IrisLootEvent$2 BUKKIT_API org.bukkit.inventory.InventoryHolder
|
||||||
|
art.arcane.iris.core.lifecycle.PaperLibBootstrap PAPERLIB_API io.papermc.lib.environments.Environment
|
||||||
|
art.arcane.iris.core.lifecycle.PaperLibBootstrap$ModernPaperEnvironment PAPERLIB_API io.papermc.lib.environments.PaperEnvironment
|
||||||
|
art.arcane.iris.core.link.ExternalDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.CraftEngineDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.EcoItemsDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.ExecutableItemsDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.HMCLeavesDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.ItemAdderDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.KGeneratorsDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.MMOItemsDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.MythicCrucibleDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.MythicMobsDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisBiomeCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
|
||||||
|
art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisRegionCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
|
||||||
|
art.arcane.iris.core.link.data.NexoDataProvider BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.nms.INMS BUKKIT_API org.bukkit.Bukkit
|
||||||
|
art.arcane.iris.core.project.IrisProject BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.core.runtime.WorldRuntimeControlService BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.core.service.BoardSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.BoardSVC$PlayerBoard BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.core.service.ExternalDataSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.GlobalCacheSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.LogFilterSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.ObjectSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.ObjectStudioSaveService BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.PreservationSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.StudioSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.service.TreeSVC BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.core.tools.IrisToolbelt BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.core.tools.IrisWorldCreator BUKKIT_API org.bukkit.generator.ChunkGenerator
|
||||||
|
art.arcane.iris.core.tools.TreePlausibilizer BUKKIT_API org.bukkit.Keyed
|
||||||
|
art.arcane.iris.engine.IrisWorldManager BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.engine.framework.EngineAssignedWorldManager BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.engine.framework.placer.WorldObjectPlacer BUKKIT_API org.bukkit.event.Event
|
||||||
|
art.arcane.iris.engine.object.IrisDirection$11 BUKKIT_API org.bukkit.block.BlockFace
|
||||||
|
art.arcane.iris.engine.object.IrisEffect$BukkitFx BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.engine.object.IrisEntity$BukkitOps BUKKIT_API org.bukkit.loot.LootTable
|
||||||
|
art.arcane.iris.engine.object.IrisEntity$BukkitOps$1 BUKKIT_API org.bukkit.loot.LootTable
|
||||||
|
art.arcane.iris.engine.object.IrisObjectRotation$1 BUKKIT_API org.bukkit.block.BlockFace
|
||||||
|
art.arcane.iris.engine.object.IrisObjectRotation$Faces BUKKIT_API org.bukkit.block.BlockFace
|
||||||
|
art.arcane.iris.engine.object.LegacyTileData$BannerHandler BUKKIT_API org.bukkit.Keyed
|
||||||
|
art.arcane.iris.engine.object.LegacyTileData$Handlers BUKKIT_API org.bukkit.block.BlockState
|
||||||
|
art.arcane.iris.engine.object.LegacyTileData$SignHandler BUKKIT_API org.bukkit.Keyed
|
||||||
|
art.arcane.iris.engine.object.LegacyTileData$SignTags BUKKIT_API org.bukkit.Tag
|
||||||
|
art.arcane.iris.engine.object.LegacyTileData$SignTags$1 BUKKIT_API org.bukkit.Tag
|
||||||
|
art.arcane.iris.engine.platform.BukkitChunkGenerator BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.engine.platform.BukkitChunkGenerator$2 BUKKIT_API org.bukkit.HeightMap
|
||||||
|
art.arcane.iris.engine.platform.DummyBiomeProvider BUKKIT_API org.bukkit.generator.BiomeProvider
|
||||||
|
art.arcane.iris.engine.platform.DummyChunkGenerator BUKKIT_API org.bukkit.generator.ChunkGenerator
|
||||||
|
art.arcane.iris.engine.platform.EngineBukkitOps BUKKIT_API org.bukkit.entity.Entity
|
||||||
|
art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator BUKKIT_API org.bukkit.Material
|
||||||
|
art.arcane.iris.platform.bukkit.BukkitBlockResolution BUKKIT_API org.bukkit.Material
|
||||||
|
art.arcane.iris.platform.bukkit.BukkitPlatform BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.util.common.data.IrisCustomData BUKKIT_API org.bukkit.block.data.BlockData
|
||||||
|
art.arcane.iris.util.common.data.registry.Attributes BUKKIT_API org.bukkit.attribute.Attribute
|
||||||
|
art.arcane.iris.util.common.data.registry.Materials BUKKIT_API org.bukkit.Material
|
||||||
|
art.arcane.iris.util.common.data.registry.Particles BUKKIT_API org.bukkit.Particle
|
||||||
|
art.arcane.iris.util.common.director.handlers.VectorHandler BUKKIT_API org.bukkit.util.Vector
|
||||||
|
art.arcane.iris.util.common.format.C$DyeMaps BUKKIT_API org.bukkit.DyeColor
|
||||||
|
art.arcane.iris.util.common.inventorygui.WindowResolution BUKKIT_API org.bukkit.event.inventory.InventoryType
|
||||||
|
art.arcane.iris.util.common.misc.Bindings BUKKIT_API org.bukkit.plugin.Plugin
|
||||||
|
art.arcane.iris.util.common.misc.ServerProperties SERVER_RUNTIME_FILE server.properties
|
||||||
|
art.arcane.iris.util.common.misc.SlimJar BUKKIT_API org.bukkit.plugin.Plugin
|
||||||
|
art.arcane.iris.util.common.plugin.CommandDummy BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.util.common.plugin.Controller BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.util.common.plugin.IController BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.util.common.plugin.IrisService BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.util.common.plugin.RouterCommand BUKKIT_API org.bukkit.command.Command
|
||||||
|
art.arcane.iris.util.common.plugin.VolmitPlugin BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.util.common.plugin.VolmitSender BUKKIT_API org.bukkit.command.CommandSender
|
||||||
|
art.arcane.iris.util.common.plugin.chunk.ChunkTickets BUKKIT_API org.bukkit.event.Listener
|
||||||
|
art.arcane.iris.util.nbt.common.mca.NBTWorld$Holder BUKKIT_API org.bukkit.Material
|
||||||
|
art.arcane.iris.util.project.sentry.ServerID$Holder BUKKIT_API org.bukkit.Bukkit
|
||||||
|
@@ -0,0 +1,204 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class ClassloadProbeTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scansNamedAndGeneratedNestedClasses() throws Exception {
|
||||||
|
Path classesRoot = temporaryFolder.newFolder("classes").toPath();
|
||||||
|
Path packageRoot = Files.createDirectories(classesRoot.resolve("art/arcane/iris"));
|
||||||
|
Files.createFile(packageRoot.resolve("Outer.class"));
|
||||||
|
Files.createFile(packageRoot.resolve("Outer$Nested.class"));
|
||||||
|
Files.createFile(packageRoot.resolve("Outer$1.class"));
|
||||||
|
Files.createFile(packageRoot.resolve("ignored.txt"));
|
||||||
|
|
||||||
|
List<String> names = ClassloadProbe.scanClassNames(classesRoot);
|
||||||
|
|
||||||
|
assertEquals(List.of(
|
||||||
|
"art.arcane.iris.Outer",
|
||||||
|
"art.arcane.iris.Outer$1",
|
||||||
|
"art.arcane.iris.Outer$Nested"
|
||||||
|
), names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scansEmptyClassDirectory() throws Exception {
|
||||||
|
Path classesRoot = temporaryFolder.newFolder("empty-classes").toPath();
|
||||||
|
|
||||||
|
assertTrue(ClassloadProbe.scanClassNames(classesRoot).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsMatchingReviewedFailure() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ClassNotFoundException",
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
|
||||||
|
ClassloadProbe.Review review = ClassloadProbe.review(
|
||||||
|
Map.of("art.arcane.iris.Allowed", failure),
|
||||||
|
Map.of("art.arcane.iris.Allowed", allowance)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(review.passes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsUnlistedFailure() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ClassNotFoundException",
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
|
||||||
|
ClassloadProbe.Review review = ClassloadProbe.review(
|
||||||
|
Map.of("art.arcane.iris.Unlisted", failure),
|
||||||
|
Map.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(review.passes());
|
||||||
|
assertTrue(review.unexpected().containsKey("art.arcane.iris.Unlisted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsDifferentMissingClassInReviewedNamespace() {
|
||||||
|
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ClassNotFoundException",
|
||||||
|
"org.bukkit.Material"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
|
||||||
|
ClassloadProbe.Review review = ClassloadProbe.review(
|
||||||
|
Map.of("art.arcane.iris.Changed", changedFailure),
|
||||||
|
Map.of("art.arcane.iris.Changed", allowance)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(review.passes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsChangedFailureNamespace() {
|
||||||
|
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ClassNotFoundException",
|
||||||
|
"io.papermc.lib.environments.Environment"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
|
||||||
|
ClassloadProbe.Review review = ClassloadProbe.review(
|
||||||
|
Map.of("art.arcane.iris.Changed", changedFailure),
|
||||||
|
Map.of("art.arcane.iris.Changed", allowance)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(review.passes());
|
||||||
|
assertTrue(review.unexpected().containsKey("art.arcane.iris.Changed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsNonMissingClassFailure() {
|
||||||
|
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.IllegalStateException",
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(allowance.matches(changedFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsBukkitFailureCachedByClassInitialization() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ExceptionInInitializerError",
|
||||||
|
"Exception java.lang.NoClassDefFoundError: org/bukkit/Material [in thread \"main\"]"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.Material"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(allowance.matches(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsDifferentBukkitFailureCachedByClassInitialization() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ExceptionInInitializerError",
|
||||||
|
"Exception java.lang.NoClassDefFoundError: org/bukkit/World [in thread \"main\"]"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.Material"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(allowance.matches(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsDifferentNamespaceCachedByClassInitialization() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ExceptionInInitializerError",
|
||||||
|
"Exception java.lang.NoClassDefFoundError: io/papermc/lib/environments/Environment [in thread \"main\"]"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.Material"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertFalse(allowance.matches(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsMythicMobsApiNamespace() {
|
||||||
|
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
|
||||||
|
"java.lang.ClassNotFoundException",
|
||||||
|
"io.lumine.mythic.api.skills.conditions.ILocationCondition"
|
||||||
|
);
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.MYTHICMOBS_API,
|
||||||
|
"io.lumine.mythic.api.skills.conditions.ILocationCondition"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(allowance.matches(failure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsStaleAllowance() {
|
||||||
|
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
|
||||||
|
ClassloadProbe.AllowanceCategory.BUKKIT_API,
|
||||||
|
"org.bukkit.event.Listener"
|
||||||
|
);
|
||||||
|
TreeMap<String, ClassloadProbe.Allowance> allowlist = new TreeMap<>();
|
||||||
|
allowlist.put("art.arcane.iris.Stale", allowance);
|
||||||
|
|
||||||
|
ClassloadProbe.Review review = ClassloadProbe.review(Map.of(), allowlist);
|
||||||
|
|
||||||
|
assertFalse(review.passes());
|
||||||
|
assertTrue(review.staleAllowances().containsKey("art.arcane.iris.Stale"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,5 +26,7 @@ public interface PlatformEntityType {
|
|||||||
|
|
||||||
String namespace();
|
String namespace();
|
||||||
|
|
||||||
|
String spawnCategory();
|
||||||
|
|
||||||
Object nativeHandle();
|
Object nativeHandle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ macOS / Linux:
|
|||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The run scripts compile the sources and create the ignored `simd-bench.jar` locally when it is
|
||||||
|
missing. Use `build.bat` or `./build.sh` to rebuild it explicitly.
|
||||||
|
|
||||||
Or invoke directly (the `--add-modules` flag is required because the Vector API
|
Or invoke directly (the `--add-modules` flag is required because the Vector API
|
||||||
is still an incubator module):
|
is still an incubator module):
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user