mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merge master
# Conflicts: # core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java
This commit is contained in:
commit
73af3efe12
34 files changed
+687
-62
No files matched your search
@@ -201,6 +201,7 @@ dependencies {
|
|||||||
libs.asm,
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ dependencies {
|
|||||||
libs.asm,
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-4
@@ -391,10 +391,7 @@ public final class ModdedEngineBootstrap {
|
|||||||
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new NativeStateMerger(ModdedBlockResolution.BLOCKS)::merge);
|
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new NativeStateMerger(ModdedBlockResolution.BLOCKS)::merge);
|
||||||
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
|
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
|
||||||
|
|
||||||
NativeTileReader nativeTileReader = new NativeTileReader(() -> {
|
NativeTileReader nativeTileReader = NativeTileReader.forServer(boundLoader::currentServer);
|
||||||
NativeModdedServer server = boundLoader.currentServer();
|
|
||||||
return server == null ? null : server.registryAccess();
|
|
||||||
});
|
|
||||||
TileData.TileReader previousTileReader = TileData.bindPlatformReader(in -> ModdedTileData.wrap(nativeTileReader.read(in)));
|
TileData.TileReader previousTileReader = TileData.bindPlatformReader(in -> ModdedTileData.wrap(nativeTileReader.read(in)));
|
||||||
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
|
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ import art.arcane.iris.spi.PlatformScheduler;
|
|||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
|
||||||
import net.minecraft.core.HolderLookup;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -64,7 +62,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.registries = new ModdedRegistries(
|
this.registries = new ModdedRegistries(
|
||||||
new NativeRegistryAccess(new NativeRegistryAccess.Configuration(
|
new NativeRegistryAccess(new NativeRegistryAccess.Configuration(
|
||||||
ModdedPlatform::nativeRegistries, ModdedPlatform::reloadableRegistries,
|
ModdedEngineBootstrap::currentServer,
|
||||||
ModdedRegistries::warnNotReady)),
|
ModdedRegistries::warnNotReady)),
|
||||||
ModdedPlatform::generationRegistry);
|
ModdedPlatform::generationRegistry);
|
||||||
this.scheduler = new ModdedScheduler();
|
this.scheduler = new ModdedScheduler();
|
||||||
@@ -72,16 +70,6 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HolderLookup.Provider nativeRegistries() {
|
|
||||||
NativeModdedServer server = ModdedEngineBootstrap.currentServer();
|
|
||||||
return server == null ? null : server.registryAccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HolderLookup.Provider reloadableRegistries() {
|
|
||||||
NativeModdedServer server = ModdedEngineBootstrap.currentServer();
|
|
||||||
return server == null ? null : server.reloadableRegistries();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void errorSink(Consumer<Throwable> sink) {
|
public static void errorSink(Consumer<Throwable> sink) {
|
||||||
ERROR_SINK = sink;
|
ERROR_SINK = sink;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"pack": {
|
||||||
|
"description": "Iris client resources and mod data",
|
||||||
|
"min_format": [88, 0],
|
||||||
|
"max_format": [107, 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -72,10 +72,10 @@ public class IrisModLanguageAssetsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> resourceFiles() throws Exception {
|
private Set<String> resourceFiles() throws Exception {
|
||||||
URL resource = IrisModLanguageAssetsTest.class.getClassLoader().getResource(ROOT);
|
URL resource = IrisModLanguageAssetsTest.class.getClassLoader().getResource(ROOT + "en_us.json");
|
||||||
assertNotNull("Missing mod language resource directory: " + ROOT, resource);
|
assertNotNull("Missing English mod language resource", resource);
|
||||||
assertEquals("Mod language resources must resolve to a directory on disk", "file", resource.getProtocol());
|
assertEquals("Mod language resources must resolve to a directory on disk", "file", resource.getProtocol());
|
||||||
Path directory = Path.of(resource.toURI());
|
Path directory = Path.of(resource.toURI()).getParent();
|
||||||
try (Stream<Path> paths = Files.list(directory)) {
|
try (Stream<Path> paths = Files.list(directory)) {
|
||||||
return paths
|
return paths
|
||||||
.filter(Files::isRegularFile)
|
.filter(Files::isRegularFile)
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.packs.PackType;
|
||||||
|
import net.minecraft.server.packs.metadata.pack.PackMetadataSection;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedPackMetadataTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void metadataLoadsClientResourcesAndServerDataForCurrentMinecraft() throws IOException {
|
||||||
|
try (InputStream resource = ModdedPackMetadataTest.class.getResourceAsStream("/pack.mcmeta")) {
|
||||||
|
assertNotNull(resource);
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(resource, StandardCharsets.UTF_8)) {
|
||||||
|
JsonObject metadata = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
for (PackType type : PackType.values()) {
|
||||||
|
PackMetadataSection section = PackMetadataSection.forPackType(type).codec()
|
||||||
|
.parse(JsonOps.INSTANCE, metadata.get("pack")).getOrThrow();
|
||||||
|
assertEquals("Iris client resources and mod data", section.description().getString());
|
||||||
|
assertTrue(type.toString(), section.supportedFormats().isValueInRange(
|
||||||
|
SharedConstants.getCurrentVersion().packVersion(type)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -62,9 +63,11 @@ public class ModdedStructureTemplateCacheTest {
|
|||||||
config = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
config = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
assertTrue(config.contains("\"StructureTemplatePaletteConcurrencyMixin\""));
|
assertTrue(config.contains("\"StructureTemplatePaletteConcurrencyMixin\""));
|
||||||
assertNotNull(Class.forName(
|
String mixin = "art/arcane/volmlib/nativelib/minecraft26_2/modded/mixin/StructureTemplatePaletteConcurrencyMixin";
|
||||||
"art.arcane.volmlib.nativelib.minecraft26_2.modded.mixin.StructureTemplatePaletteConcurrencyMixin",
|
try (InputStream input = ModdedStructureTemplateCacheTest.class.getClassLoader()
|
||||||
false,
|
.getResourceAsStream(mixin + ".class")) {
|
||||||
ModdedStructureTemplateCacheTest.class.getClassLoader()));
|
assertNotNull(input);
|
||||||
|
assertEquals(mixin, new ClassReader(input).getClassName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -41,7 +41,7 @@ public class ModdedTileParityTest {
|
|||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
Bootstrap.bootStrap();
|
Bootstrap.bootStrap();
|
||||||
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||||
NativeTileReader nativeTileReader = new NativeTileReader(() -> null);
|
NativeTileReader nativeTileReader = NativeTileReader.forServer(() -> null);
|
||||||
TileData.bindPlatformReader(in -> ModdedTileData.wrap(nativeTileReader.read(in)));
|
TileData.bindPlatformReader(in -> ModdedTileData.wrap(nativeTileReader.read(in)));
|
||||||
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
|
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -11,9 +11,12 @@ import art.arcane.iris.generation.decoration.IrisSurface;
|
|||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -34,6 +37,11 @@ public class ModdedWaterEntitySpawnTest {
|
|||||||
public static void bootstrapMinecraftRegistries() {
|
public static void bootstrapMinecraftRegistries() {
|
||||||
SharedConstants.tryDetectVersion();
|
SharedConstants.tryDetectVersion();
|
||||||
Bootstrap.bootStrap();
|
Bootstrap.bootStrap();
|
||||||
|
for (Block block : BuiltInRegistries.BLOCK) {
|
||||||
|
for (BlockState state : block.getStateDefinition().getPossibleStates()) {
|
||||||
|
state.initCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+42
-3
@@ -1,20 +1,31 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class NativeRegistryAccessTest {
|
public class NativeRegistryAccessTest {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
@@ -25,7 +36,7 @@ public class NativeRegistryAccessTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void statePropertiesPreserveTypesDefaultsAndSharedGroups() {
|
public void statePropertiesPreserveTypesDefaultsAndSharedGroups() {
|
||||||
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, () -> null, message -> {}));
|
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, message -> {}));
|
||||||
Map<String, List<NativeBlockProperty>> properties = access.blockStateProperties();
|
Map<String, List<NativeBlockProperty>> properties = access.blockStateProperties();
|
||||||
|
|
||||||
assertSame(properties.get("minecraft:stone"), properties.get("minecraft:dirt"));
|
assertSame(properties.get("minecraft:stone"), properties.get("minecraft:dirt"));
|
||||||
@@ -41,7 +52,7 @@ public class NativeRegistryAccessTest {
|
|||||||
@Test
|
@Test
|
||||||
public void staticRegistriesResolveWithoutAnActiveServerAndDynamicReadsStayEmpty() {
|
public void staticRegistriesResolveWithoutAnActiveServerAndDynamicReadsStayEmpty() {
|
||||||
List<String> warnings = new ArrayList<>();
|
List<String> warnings = new ArrayList<>();
|
||||||
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, () -> null, warnings::add));
|
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(() -> null, warnings::add));
|
||||||
|
|
||||||
assertEquals("minecraft:diamond_sword", access.item("DIAMOND SWORD").key());
|
assertEquals("minecraft:diamond_sword", access.item("DIAMOND SWORD").key());
|
||||||
assertEquals("creature", access.entity("minecraft:cow").spawnCategory());
|
assertEquals("creature", access.entity("minecraft:cow").spawnCategory());
|
||||||
@@ -49,6 +60,34 @@ public class NativeRegistryAccessTest {
|
|||||||
assertTrue(access.biomeKeys().isEmpty());
|
assertTrue(access.biomeKeys().isEmpty());
|
||||||
assertTrue(access.structureKeys().isEmpty());
|
assertTrue(access.structureKeys().isEmpty());
|
||||||
assertTrue(access.enchantmentKeys().isEmpty());
|
assertTrue(access.enchantmentKeys().isEmpty());
|
||||||
assertEquals(List.of("biome", "structure", "enchantment"), warnings);
|
assertTrue(access.lootTableKeys().isEmpty());
|
||||||
|
assertEquals(List.of("biome", "structure", "enchantment", "loot table"), warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dynamicRegistriesFollowServerStartupReloadAndShutdown() {
|
||||||
|
AtomicReference<NativeModdedServer> activeServer = new AtomicReference<>();
|
||||||
|
List<String> warnings = new ArrayList<>();
|
||||||
|
NativeRegistryAccess access = new NativeRegistryAccess(new NativeRegistryAccess.Configuration(activeServer::get, warnings::add));
|
||||||
|
assertTrue(access.lootTableKeys().isEmpty());
|
||||||
|
|
||||||
|
MinecraftServer server = mock(MinecraftServer.class, RETURNS_DEEP_STUBS);
|
||||||
|
when(server.registryAccess().lookupOrThrow(Registries.BIOME).listElementIds())
|
||||||
|
.thenAnswer(invocation -> Stream.of(ResourceKey.create(Registries.BIOME, Identifier.parse("example:biome"))));
|
||||||
|
when(server.reloadableRegistries().lookup().lookupOrThrow(Registries.LOOT_TABLE).listElementIds())
|
||||||
|
.thenAnswer(invocation -> Stream.of(ResourceKey.create(Registries.LOOT_TABLE, Identifier.parse("example:initial"))));
|
||||||
|
activeServer.set(NativeModdedServer.fromHandle(server));
|
||||||
|
|
||||||
|
assertEquals(List.of("example:biome"), access.biomeKeys());
|
||||||
|
assertEquals(List.of("example:initial"), access.lootTableKeys());
|
||||||
|
HolderLookup.Provider reloadedRegistries = mock(HolderLookup.Provider.class, RETURNS_DEEP_STUBS);
|
||||||
|
when(reloadedRegistries.lookupOrThrow(Registries.LOOT_TABLE).listElementIds())
|
||||||
|
.thenAnswer(invocation -> Stream.of(ResourceKey.create(Registries.LOOT_TABLE, Identifier.parse("example:reloaded"))));
|
||||||
|
when(server.reloadableRegistries().lookup()).thenReturn(reloadedRegistries);
|
||||||
|
assertEquals(List.of("example:reloaded"), access.lootTableKeys());
|
||||||
|
activeServer.set(null);
|
||||||
|
assertTrue(access.biomeKeys().isEmpty());
|
||||||
|
assertTrue(access.lootTableKeys().isEmpty());
|
||||||
|
assertEquals(List.of("loot table", "biome", "loot table"), warnings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,6 +173,7 @@ dependencies {
|
|||||||
libs.asm,
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
|
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
libs.jaxen
|
libs.jaxen
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -576,6 +576,14 @@ tasks.named('check').configure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> commonModdedArtifactEntries = [
|
List<String> commonModdedArtifactEntries = [
|
||||||
|
'pack.mcmeta',
|
||||||
|
'com/fulcrumgenomics/jlibdeflate/LibdeflateDecompressor.class',
|
||||||
|
'com/fulcrumgenomics/jlibdeflate/LibdeflateException.class',
|
||||||
|
'native/linux-aarch64/libjlibdeflate.so',
|
||||||
|
'native/linux-x86_64/libjlibdeflate.so',
|
||||||
|
'native/osx-aarch64/libjlibdeflate.dylib',
|
||||||
|
'native/osx-x86_64/libjlibdeflate.dylib',
|
||||||
|
'native/windows-x86_64/jlibdeflate.dll',
|
||||||
'META-INF/iris/generation-kernels/catalog.tsv',
|
'META-INF/iris/generation-kernels/catalog.tsv',
|
||||||
'META-INF/iris/generation-kernels/abi-1.revision',
|
'META-INF/iris/generation-kernels/abi-1.revision',
|
||||||
'META-INF/services/art.arcane.iris.localization.IrisMessageContributor',
|
'META-INF/services/art.arcane.iris.localization.IrisMessageContributor',
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ public final class ModdedArtifactVerifier {
|
|||||||
throw new GradleException(artifact.getName() + " is missing " + requiredEntry);
|
throw new GradleException(artifact.getName() + " is missing " + requiredEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (jar.getJarEntry("assets/irisworldgen/lang/en_us.json") != null
|
||||||
|
&& jar.getJarEntry("pack.mcmeta") == null) {
|
||||||
|
throw new GradleException(artifact.getName()
|
||||||
|
+ " ships Iris client language resources without pack.mcmeta");
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> bundledRuntimeEntries = new LinkedHashSet<>();
|
Set<String> bundledRuntimeEntries = new LinkedHashSet<>();
|
||||||
Set<String> privateReferenceClasses = new LinkedHashSet<>();
|
Set<String> privateReferenceClasses = new LinkedHashSet<>();
|
||||||
|
|||||||
@@ -44,6 +44,28 @@ public class ModdedArtifactVerifierTest {
|
|||||||
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
|
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsClientLanguagesWithoutPackMetadata() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
|
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
|
||||||
|
assertTrue(failure.getMessage().contains("client language resources without pack.mcmeta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptsClientLanguagesWithPackMetadata() throws Exception {
|
||||||
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
entries.put("pack.mcmeta", ("{\"pack\":{\"description\":\"Iris\","
|
||||||
|
+ "\"min_format\":[88,0],\"max_format\":[107,1]}}")
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsRelocatedRuntimeReference() throws Exception {
|
public void rejectsRelocatedRuntimeReference() throws Exception {
|
||||||
Map<String, byte[]> entries = validEntries();
|
Map<String, byte[]> entries = validEntries();
|
||||||
|
|||||||
@@ -1089,16 +1089,18 @@ public class IrisComplex implements DataProvider {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
HydrologyColumnLayer original = sample.layers().get(index);
|
HydrologyColumnLayer original = sample.layers().get(index);
|
||||||
int head = (int) Math.round(transitionDisplacement.fluidHeight(sample.x(), sample.z(), layer.fluidHeadY()));
|
int displacedHead = (int) Math.round(
|
||||||
|
transitionDisplacement.fluidHeight(sample.x(), sample.z(), original.fluidHeadY()));
|
||||||
|
boolean channel = original.channel() && layer.bedY() <= displacedHead;
|
||||||
|
int head = channel ? displacedHead : layer.bedY();
|
||||||
if ((tapered.naturalHeight() < seaLevel || tapered.naturalHeight() == seaLevel && head > seaLevel)
|
if ((tapered.naturalHeight() < seaLevel || tapered.naturalHeight() == seaLevel && head > seaLevel)
|
||||||
&& (original.terrainOwned() || original.fluidOwned() || original.grading() || original.shore())) {
|
&& (original.terrainOwned() || original.fluidOwned() || original.grading() || original.shore())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
boolean dryChannel = original.channel() && layer.bedY() > head;
|
|
||||||
layers.add(new HydrologyColumnLayer(layer.feature(), layer.bedY(),
|
layers.add(new HydrologyColumnLayer(layer.feature(), layer.bedY(),
|
||||||
head, head, original.channel() && !dryChannel, layer.shore(), layer.grading(), original.connectedFluid() && !dryChannel,
|
head, head, channel, layer.shore(), layer.grading(), original.connectedFluid() && channel,
|
||||||
original.fallingFluid() && !dryChannel, original.receivingPool() && !dryChannel,
|
original.fallingFluid() && channel, original.receivingPool() && channel,
|
||||||
layer.terrainOwned(), original.fluidOwned() && !dryChannel, layer.oceanApron(),
|
layer.terrainOwned(), original.fluidOwned() && channel, layer.oceanApron(),
|
||||||
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
|
layer.profileKey(), layer.surfaceBiomeKey(), layer.mouthBiomeKey(), layer.shoreBiomeKey(),
|
||||||
layer.bankBiomeKey(), layer.floodedCaveBiomeKey()));
|
layer.bankBiomeKey(), layer.floodedCaveBiomeKey()));
|
||||||
}
|
}
|
||||||
@@ -1140,20 +1142,21 @@ public class IrisComplex implements DataProvider {
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
|
int bed = GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight);
|
||||||
boolean dryChannel = layer.channel() && bed > layer.fluidHeadY();
|
boolean channel = layer.channel() && bed <= layer.fluidHeadY();
|
||||||
|
int head = channel ? layer.fluidHeadY() : bed;
|
||||||
return new HydrologyColumnLayer(
|
return new HydrologyColumnLayer(
|
||||||
layer.feature(),
|
layer.feature(),
|
||||||
bed,
|
bed,
|
||||||
layer.fluidHeadY(),
|
head,
|
||||||
layer.ceilingY(),
|
head,
|
||||||
layer.channel() && !dryChannel,
|
channel,
|
||||||
layer.shore(),
|
layer.shore(),
|
||||||
layer.grading(),
|
layer.grading(),
|
||||||
layer.connectedFluid() && !dryChannel,
|
layer.connectedFluid() && channel,
|
||||||
layer.fallingFluid() && !dryChannel,
|
layer.fallingFluid() && channel,
|
||||||
layer.receivingPool() && !dryChannel,
|
layer.receivingPool() && channel,
|
||||||
layer.terrainOwned(),
|
layer.terrainOwned(),
|
||||||
layer.fluidOwned() && !dryChannel,
|
layer.fluidOwned() && channel,
|
||||||
layer.oceanApron(),
|
layer.oceanApron(),
|
||||||
layer.profileKey(),
|
layer.profileKey(),
|
||||||
layer.surfaceBiomeKey(),
|
layer.surfaceBiomeKey(),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import java.util.concurrent.Semaphore;
|
|||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||||
import art.arcane.iris.world.IrisWorlds;
|
import art.arcane.iris.world.IrisWorlds;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
@@ -1290,6 +1291,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
|
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
|
||||||
|
try {
|
||||||
|
generateNoiseChunk(world, random, x, z, d);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
PregeneratorJob.reportChunkGenerationFailure(this.world.identity(), x, z, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateNoiseChunk(WorldInfo world, Random random, int x, int z, ChunkGenerator.ChunkData d) {
|
||||||
if (closing) {
|
if (closing) {
|
||||||
throw new IllegalStateException("Iris chunk generation was rejected while the generator is closing.");
|
throw new IllegalStateException("Iris chunk generation was rejected while the generator is closing.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,14 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return instance.get();
|
return instance.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void reportChunkGenerationFailure(String worldIdentity, int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
PregeneratorJob inst = instance.get();
|
||||||
|
if (inst == null || worldIdentity == null || !worldIdentity.equals(inst.worldIdentity())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inst.pregenerator.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean pauseResume() {
|
public static boolean pauseResume() {
|
||||||
PregeneratorJob inst = instance.get();
|
PregeneratorJob inst = instance.get();
|
||||||
return inst != null && inst.togglePause();
|
return inst != null && inst.togglePause();
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
|
|||||||
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
method.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||||
method.onPregenStart(centerBlockX, centerBlockZ);
|
method.onPregenStart(centerBlockX, centerBlockZ);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.world.pregen;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorkerPool;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorkerPool;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import art.arcane.iris.world.IrisPaperLikeBackendMode;
|
import art.arcane.iris.world.IrisPaperLikeBackendMode;
|
||||||
import art.arcane.iris.world.IrisRuntimeSchedulerMode;
|
import art.arcane.iris.world.IrisRuntimeSchedulerMode;
|
||||||
@@ -55,6 +56,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public class AsyncPregenMethod implements PregeneratorMethod {
|
public class AsyncPregenMethod implements PregeneratorMethod {
|
||||||
// THREAD_COUNT records the pool's original size while boosted; BOOST_HOLDERS counts live
|
// THREAD_COUNT records the pool's original size while boosted; BOOST_HOLDERS counts live
|
||||||
@@ -64,7 +66,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
private static final AtomicInteger BOOST_HOLDERS = new AtomicInteger();
|
||||||
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
|
private static final int ADAPTIVE_SLOW_REQUEST_STEP = 3;
|
||||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||||
private static final long CLOSE_DRAIN_WARNING_SECONDS = 60L;
|
private static final long CLOSE_DRAIN_WARNING_SECONDS = 20L;
|
||||||
|
private static final long CLOSE_DRAIN_TIMEOUT_SECONDS = 60L;
|
||||||
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
private static final long FLUSH_TIMEOUT_SECONDS = 120L;
|
||||||
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
|
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
|
||||||
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
|
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
|
||||||
@@ -113,6 +116,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private final AtomicLong failed = new AtomicLong();
|
private final AtomicLong failed = new AtomicLong();
|
||||||
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
||||||
private final AtomicBoolean closing = new AtomicBoolean();
|
private final AtomicBoolean closing = new AtomicBoolean();
|
||||||
|
private final PregenInFlightRequests inFlightRequests = new PregenInFlightRequests();
|
||||||
|
private final AtomicReference<GenerationFailure> generationFailure = new AtomicReference<>();
|
||||||
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
||||||
private volatile Engine metricsEngine;
|
private volatile Engine metricsEngine;
|
||||||
private volatile Mantle cachedMantle;
|
private volatile Mantle cachedMantle;
|
||||||
@@ -515,10 +520,20 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
|
private void completeChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) {
|
||||||
|
PregenListener owner = inFlightRequests.settle(PregenInFlightRequests.key(x, z));
|
||||||
|
if (owner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settleChunk(x, z, owner, chunk, throwable, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void settleChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable, boolean reportFailure) {
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
|
if (reportFailure) {
|
||||||
onChunkFutureFailure(x, z, throwable);
|
onChunkFutureFailure(x, z, throwable);
|
||||||
|
}
|
||||||
onChunkFailedToLoad(x, z);
|
onChunkFailedToLoad(x, z);
|
||||||
listener.onChunkFailed(x, z);
|
listener.onChunkFailed(x, z);
|
||||||
} else if (chunk == null) {
|
} else if (chunk == null) {
|
||||||
@@ -718,11 +733,57 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
+ " stalledForMs=" + stalledFor;
|
+ " stalledForMs=" + stalledFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markSubmitted() {
|
private void markSubmitted(int x, int z, PregenListener listener) {
|
||||||
|
inFlightRequests.add(PregenInFlightRequests.key(x, z), listener);
|
||||||
submitted.incrementAndGet();
|
submitted.incrementAndGet();
|
||||||
inFlight.incrementAndGet();
|
inFlight.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
GenerationFailure recorded = new GenerationFailure(chunkX, chunkZ, failure);
|
||||||
|
if (generationFailure.compareAndSet(null, recorded)) {
|
||||||
|
IrisLogging.error("Async pregen aborting: Iris failed to generate chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " and the server's chunk system will not complete the " + inFlightRequests.size()
|
||||||
|
+ " outstanding chunk request(s). " + metricsSnapshot());
|
||||||
|
}
|
||||||
|
failOutstandingRequests(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failOutstandingRequests(Throwable failure) {
|
||||||
|
Map<Long, PregenListener> outstanding = inFlightRequests.drain();
|
||||||
|
for (Map.Entry<Long, PregenListener> entry : outstanding.entrySet()) {
|
||||||
|
long key = entry.getKey();
|
||||||
|
settleChunk(PregenInFlightRequests.chunkX(key), PregenInFlightRequests.chunkZ(key), entry.getValue(),
|
||||||
|
null, failure, false);
|
||||||
|
}
|
||||||
|
admission.wake();
|
||||||
|
backpressure.signalProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwIfGenerationFailed() {
|
||||||
|
GenerationFailure failure = generationFailure.get();
|
||||||
|
if (failure != null) {
|
||||||
|
throw new IllegalStateException("Pregen aborted: Iris failed to generate chunk " + failure.chunkX() + ","
|
||||||
|
+ failure.chunkZ() + " in world " + world.getName()
|
||||||
|
+ " and the server's chunk system stopped completing requests.", failure.cause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void abandonOutstandingRequests(PregenAdmissionGate.Drain drain) {
|
||||||
|
Map<Long, PregenListener> abandoned = inFlightRequests.drain();
|
||||||
|
IrisLogging.error("Async pregen abandoned " + abandoned.size() + " chunk request(s) that did not complete within "
|
||||||
|
+ CLOSE_DRAIN_TIMEOUT_SECONDS + "s of closing (" + drain.outstanding()
|
||||||
|
+ " permit(s) still held). They are not cached and regenerate on the next run. " + metricsSnapshot());
|
||||||
|
IllegalStateException cause = new IllegalStateException(
|
||||||
|
"Async chunk request never completed before the pregen closed in world " + world.getName() + ".");
|
||||||
|
for (Map.Entry<Long, PregenListener> entry : abandoned.entrySet()) {
|
||||||
|
long key = entry.getKey();
|
||||||
|
settleChunk(PregenInFlightRequests.chunkX(key), PregenInFlightRequests.chunkZ(key), entry.getValue(),
|
||||||
|
null, cause, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void markFinished(boolean success) {
|
private void markFinished(boolean success) {
|
||||||
if (success) {
|
if (success) {
|
||||||
completed.incrementAndGet();
|
completed.incrementAndGet();
|
||||||
@@ -840,11 +901,17 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
// A stop request interrupts the pregen worker; shield the drain and flush so chunks still hit disk.
|
||||||
boolean interrupted = Thread.interrupted();
|
boolean interrupted = Thread.interrupted();
|
||||||
try {
|
try {
|
||||||
interrupted |= admission.awaitDrain(
|
PregenAdmissionGate.Drain drain = admission.awaitDrain(
|
||||||
CLOSE_DRAIN_WARNING_SECONDS,
|
CLOSE_DRAIN_WARNING_SECONDS,
|
||||||
TimeUnit.SECONDS,
|
TimeUnit.SECONDS,
|
||||||
|
CLOSE_DRAIN_TIMEOUT_SECONDS,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
() -> IrisLogging.warn("Async pregen is still draining outstanding chunks. " + metricsSnapshot())
|
||||||
);
|
);
|
||||||
|
interrupted |= drain.interrupted();
|
||||||
|
if (!drain.drained()) {
|
||||||
|
abandonOutstandingRequests(drain);
|
||||||
|
}
|
||||||
syncGenerationHistory();
|
syncGenerationHistory();
|
||||||
|
|
||||||
mantleCleanup.close(MANTLE_CLEANUP_DRAIN_SECONDS, TimeUnit.SECONDS);
|
mantleCleanup.close(MANTLE_CLEANUP_DRAIN_SECONDS, TimeUnit.SECONDS);
|
||||||
@@ -889,7 +956,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCancelled() {
|
private boolean isCancelled() {
|
||||||
return closing.get() || Thread.currentThread().isInterrupted();
|
return closing.get() || generationFailure.get() != null || Thread.currentThread().isInterrupted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -917,6 +984,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateChunk(int x, int z, PregenListener listener) {
|
public void generateChunk(int x, int z, PregenListener listener) {
|
||||||
|
throwIfGenerationFailed();
|
||||||
listener.onChunkGenerating(x, z);
|
listener.onChunkGenerating(x, z);
|
||||||
if (MantleHeapPressure.overHighWater()) {
|
if (MantleHeapPressure.overHighWater()) {
|
||||||
reclaimMemory();
|
reclaimMemory();
|
||||||
@@ -947,10 +1015,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
recordPermitWait(waited.permitMs());
|
recordPermitWait(waited.permitMs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throwIfGenerationFailed();
|
||||||
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
|
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
|
||||||
retainAround(x, z);
|
retainAround(x, z);
|
||||||
markSubmitted();
|
markSubmitted(x, z, listener);
|
||||||
executor.generate(x, z, listener);
|
executor.generate(x, z, listener);
|
||||||
|
GenerationFailure failure = generationFailure.get();
|
||||||
|
if (failure != null) {
|
||||||
|
failOutstandingRequests(failure.cause());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reclaimHeapPressure() {
|
private void reclaimHeapPressure() {
|
||||||
@@ -1187,6 +1260,9 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record GenerationFailure(int chunkX, int chunkZ, Throwable cause) {
|
||||||
|
}
|
||||||
|
|
||||||
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
|
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +189,11 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
method.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||||
method.onPregenStart(centerBlockX, centerBlockZ);
|
method.onPregenStart(centerBlockX, centerBlockZ);
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ public class HybridPregenMethod implements PregeneratorMethod {
|
|||||||
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
inWorld.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
public void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||||
inWorld.onPregenStart(centerBlockX, centerBlockZ);
|
inWorld.onPregenStart(centerBlockX, centerBlockZ);
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ public class IrisPregenerator {
|
|||||||
signalState();
|
signalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
generator.onChunkGenerationFailed(chunkX, chunkZ, failure);
|
||||||
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
boolean completed = false;
|
boolean completed = false;
|
||||||
|
|||||||
@@ -111,14 +111,20 @@ public final class PregenAdmissionGate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean awaitDrain(long warningInterval, TimeUnit timeUnit, Runnable onWait) {
|
public Drain awaitDrain(long warningInterval, TimeUnit warningUnit, long deadline, TimeUnit deadlineUnit, Runnable onWait) {
|
||||||
|
long warningNanos = warningUnit.toNanos(warningInterval);
|
||||||
|
long deadlineAt = System.nanoTime() + deadlineUnit.toNanos(deadline);
|
||||||
boolean interrupted = false;
|
boolean interrupted = false;
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
while (free < permits) {
|
while (free < permits) {
|
||||||
|
long remaining = deadlineAt - System.nanoTime();
|
||||||
|
if (remaining <= 0L) {
|
||||||
|
return new Drain(false, interrupted, permits - free);
|
||||||
|
}
|
||||||
boolean signalled = false;
|
boolean signalled = false;
|
||||||
try {
|
try {
|
||||||
signalled = changed.await(warningInterval, timeUnit);
|
signalled = changed.awaitNanos(Math.min(warningNanos, remaining)) > 0L;
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
interrupted = true;
|
interrupted = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -127,10 +133,12 @@ public final class PregenAdmissionGate {
|
|||||||
onWait.run();
|
onWait.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return new Drain(true, interrupted, 0);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return interrupted;
|
public record Drain(boolean drained, boolean interrupted, int outstanding) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.world.pregen;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class PregenInFlightRequests {
|
||||||
|
private final ConcurrentHashMap<Long, PregenListener> requests = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static long key(int chunkX, int chunkZ) {
|
||||||
|
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int chunkX(long key) {
|
||||||
|
return (int) (key >> 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int chunkZ(long key) {
|
||||||
|
return (int) key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(long key, PregenListener listener) {
|
||||||
|
return requests.putIfAbsent(key, Objects.requireNonNull(listener, "listener")) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PregenListener settle(long key) {
|
||||||
|
return requests.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Long, PregenListener> drain() {
|
||||||
|
HashMap<Long, PregenListener> drained = new HashMap<>();
|
||||||
|
Iterator<Map.Entry<Long, PregenListener>> iterator = requests.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<Long, PregenListener> entry = iterator.next();
|
||||||
|
if (requests.remove(entry.getKey(), entry.getValue())) {
|
||||||
|
drained.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return drained;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return requests.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,9 @@ public interface PregeneratorMethod {
|
|||||||
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void onChunkGenerationFailed(int chunkX, int chunkZ, Throwable failure) {
|
||||||
|
}
|
||||||
|
|
||||||
default void onPregenStart(int centerBlockX, int centerBlockZ) {
|
default void onPregenStart(int centerBlockX, int centerBlockZ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+75
-8
@@ -31,13 +31,14 @@ public class IrisComplexTransitionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void tapersRiverDepthWithoutRaisingItsFluidHead() {
|
public void tapersRiverDepthWithoutRaisingItsFluidHead() {
|
||||||
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.5D);
|
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.8D);
|
||||||
HydrologyColumnLayer layer = tapered.layers().getFirst();
|
HydrologyColumnLayer layer = tapered.layers().getFirst();
|
||||||
|
|
||||||
assertEquals(73, layer.bedY());
|
assertEquals(69, layer.bedY());
|
||||||
assertEquals(70, layer.fluidHeadY());
|
assertEquals(70, layer.fluidHeadY());
|
||||||
assertEquals(70, layer.ceilingY());
|
assertEquals(70, layer.ceilingY());
|
||||||
assertEquals(73, tapered.terrainHeight());
|
assertTrue(layer.channel());
|
||||||
|
assertEquals(69, tapered.terrainHeight());
|
||||||
assertEquals("river", layer.profileKey());
|
assertEquals("river", layer.profileKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +62,10 @@ public class IrisComplexTransitionTest {
|
|||||||
HydrologyColumnLayer surface = hydrologySample().layers().getFirst();
|
HydrologyColumnLayer surface = hydrologySample().layers().getFirst();
|
||||||
HydrologyColumnSample sample = new HydrologyColumnSample(4, 6, 80, 63, false, "parent", List.of(surface, cave));
|
HydrologyColumnSample sample = new HydrologyColumnSample(4, 6, 80, 63, false, "parent", List.of(surface, cave));
|
||||||
|
|
||||||
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, 0.5D);
|
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, 0.8D);
|
||||||
|
|
||||||
assertSame(cave, tapered.layers().getFirst());
|
assertSame(cave, tapered.layers().getFirst());
|
||||||
assertEquals(73, tapered.layers().getLast().bedY());
|
assertEquals(69, tapered.layers().getLast().bedY());
|
||||||
assertEquals(70, tapered.layers().getLast().fluidHeadY());
|
assertEquals(70, tapered.layers().getLast().fluidHeadY());
|
||||||
assertEquals(70, tapered.layers().getLast().ceilingY());
|
assertEquals(70, tapered.layers().getLast().ceilingY());
|
||||||
}
|
}
|
||||||
@@ -85,6 +86,62 @@ public class IrisComplexTransitionTest {
|
|||||||
assertSame(sample, IrisComplex.transitionHydrologySample(sample, 1D, null));
|
assertSame(sample, IrisComplex.transitionHydrologySample(sample, 1D, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void taperedDryBankKeepsItsHeadOnItsBed() {
|
||||||
|
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(bankSample(364, 366), 0.5D);
|
||||||
|
HydrologyColumnLayer bank = tapered.layers().getFirst();
|
||||||
|
|
||||||
|
assertEquals(365, bank.bedY());
|
||||||
|
assertEquals(365, bank.fluidHeadY());
|
||||||
|
assertEquals(365, bank.ceilingY());
|
||||||
|
assertFalse(bank.channel());
|
||||||
|
assertTrue(bank.grading());
|
||||||
|
assertTrue(bank.terrainOwned());
|
||||||
|
assertEquals(365, tapered.terrainHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transitionedDryBankKeepsItsHeadOnItsBed() {
|
||||||
|
HydrologyColumnSample sample = bankSample(365, 380);
|
||||||
|
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
|
||||||
|
when(displacement.sample(4, 6)).thenReturn(
|
||||||
|
new TransitionDisplacementField.Sample(0D, 8D, 0.25D, 0D, 0D, 300D, "minecraft:water"));
|
||||||
|
when(displacement.fluidHeight(4, 6, 63D)).thenReturn(63D);
|
||||||
|
when(displacement.fluidHeight(4, 6, 365D)).thenReturn(364D);
|
||||||
|
|
||||||
|
HydrologyColumnSample transitioned = IrisComplex.transitionHydrologySample(sample, 1D, displacement);
|
||||||
|
HydrologyColumnLayer bank = transitioned.layers().getFirst();
|
||||||
|
|
||||||
|
assertEquals(365, bank.bedY());
|
||||||
|
assertEquals(365, bank.fluidHeadY());
|
||||||
|
assertEquals(365, bank.ceilingY());
|
||||||
|
assertFalse(bank.channel());
|
||||||
|
assertTrue(bank.terrainOwned());
|
||||||
|
assertEquals(365, transitioned.terrainHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void driedChannelHeadFollowsItsTaperedBed() {
|
||||||
|
HydrologyColumnSample sample = hydrologySample();
|
||||||
|
TransitionDisplacementField displacement = mock(TransitionDisplacementField.class);
|
||||||
|
when(displacement.sample(4, 6)).thenReturn(
|
||||||
|
new TransitionDisplacementField.Sample(0D, 8D, 0.25D, 1D, 80D, 80D, "minecraft:water"));
|
||||||
|
when(displacement.fluidHeight(4, 6, 63D)).thenReturn(63D);
|
||||||
|
when(displacement.fluidHeight(4, 6, 70D)).thenReturn(70D);
|
||||||
|
|
||||||
|
HydrologyColumnSample transitioned = IrisComplex.transitionHydrologySample(sample, 0.35D, displacement);
|
||||||
|
HydrologyColumnLayer river = transitioned.layers().getFirst();
|
||||||
|
|
||||||
|
assertEquals(75, river.bedY());
|
||||||
|
assertEquals(75, river.fluidHeadY());
|
||||||
|
assertEquals(75, river.ceilingY());
|
||||||
|
assertFalse(river.channel());
|
||||||
|
assertFalse(river.fluidOwned());
|
||||||
|
assertFalse(river.connectedFluid());
|
||||||
|
assertTrue(river.terrainOwned());
|
||||||
|
assertEquals(75, transitioned.terrainHeight());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void tinyRiverWeightsExcavateGraduallyUntilReachingUnraisedWater() {
|
public void tinyRiverWeightsExcavateGraduallyUntilReachingUnraisedWater() {
|
||||||
HydrologyColumnSample sample = hydrologySample();
|
HydrologyColumnSample sample = hydrologySample();
|
||||||
@@ -93,13 +150,14 @@ public class IrisComplexTransitionTest {
|
|||||||
double weight = step / 100D;
|
double weight = step / 100D;
|
||||||
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, weight);
|
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(sample, weight);
|
||||||
HydrologyColumnLayer layer = tapered.layers().getFirst();
|
HydrologyColumnLayer layer = tapered.layers().getFirst();
|
||||||
assertEquals(70, layer.fluidHeadY());
|
assertEquals(layer.bedY() <= 70, layer.channel());
|
||||||
|
assertEquals(layer.channel() ? 70 : layer.bedY(), layer.fluidHeadY());
|
||||||
|
assertEquals(layer.fluidHeadY(), layer.ceilingY());
|
||||||
assertTrue(layer.bedY() <= previousBed);
|
assertTrue(layer.bedY() <= previousBed);
|
||||||
assertTrue(previousBed - layer.bedY() <= 1);
|
assertTrue(previousBed - layer.bedY() <= 1);
|
||||||
assertEquals(layer.bedY(), tapered.terrainHeight());
|
assertEquals(layer.bedY(), tapered.terrainHeight());
|
||||||
assertEquals(layer.bedY() <= layer.fluidHeadY(), layer.channel());
|
|
||||||
assertEquals(layer.channel(), layer.fluidOwned());
|
assertEquals(layer.channel(), layer.fluidOwned());
|
||||||
if (layer.bedY() > layer.fluidHeadY()) {
|
if (!layer.channel()) {
|
||||||
assertFalse(layer.connectedFluid());
|
assertFalse(layer.connectedFluid());
|
||||||
assertFalse(layer.fallingFluid());
|
assertFalse(layer.fallingFluid());
|
||||||
assertFalse(layer.receivingPool());
|
assertFalse(layer.receivingPool());
|
||||||
@@ -209,6 +267,15 @@ public class IrisComplexTransitionTest {
|
|||||||
return hydrologySample(HydrologyFeatureType.SURFACE_POOL);
|
return hydrologySample(HydrologyFeatureType.SURFACE_POOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HydrologyColumnSample bankSample(int bankHeight, int naturalHeight) {
|
||||||
|
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||||
|
1L, HydrologyFeatureType.SURFACE_POOL, 2L, 3L, 1080, 346, 204, 0, -1, false);
|
||||||
|
HydrologyColumnLayer bank = new HydrologyColumnLayer(feature, bankHeight, bankHeight, bankHeight,
|
||||||
|
false, false, true, false, false, false, true, false, false,
|
||||||
|
"water", "surface", "mouth", "shore", "bank", "cave");
|
||||||
|
return new HydrologyColumnSample(4, 6, naturalHeight, 63, false, "parent", List.of(bank));
|
||||||
|
}
|
||||||
|
|
||||||
private static HydrologyColumnSample hydrologySample(HydrologyFeatureType type) {
|
private static HydrologyColumnSample hydrologySample(HydrologyFeatureType type) {
|
||||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||||
1L,
|
1L,
|
||||||
|
|||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
package art.arcane.iris.world.pregen;
|
||||||
|
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class AsyncPregenGenerationFailureTest {
|
||||||
|
@Test
|
||||||
|
public void aGenerationFailureFailsEveryOutstandingRequestAndReleasesTheirPermits() throws Exception {
|
||||||
|
Harness harness = Harness.withOutstanding(2);
|
||||||
|
IllegalStateException cause = new IllegalStateException("Accepted hydrology layer exceeds the mantle bounds");
|
||||||
|
|
||||||
|
harness.method.onChunkGenerationFailed(48, -4, cause);
|
||||||
|
|
||||||
|
assertEquals(2, harness.admission.availablePermits());
|
||||||
|
assertEquals(2, harness.failed.get());
|
||||||
|
assertEquals(0, harness.inFlight.get());
|
||||||
|
assertEquals(0, harness.requests.size());
|
||||||
|
verify(harness.listener).onChunkFailed(10, 0);
|
||||||
|
verify(harness.listener).onChunkFailed(11, 0);
|
||||||
|
|
||||||
|
IllegalStateException abort = assertThrows(IllegalStateException.class,
|
||||||
|
() -> harness.method.generateChunk(12, 0, harness.listener));
|
||||||
|
assertSame(cause, abort.getCause());
|
||||||
|
verify(harness.listener, never()).onChunkGenerating(12, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aLateCompletionForASettledRequestIsIgnored() throws Exception {
|
||||||
|
Harness harness = Harness.withOutstanding(2);
|
||||||
|
harness.method.onChunkGenerationFailed(48, -4, new IllegalStateException("boom"));
|
||||||
|
|
||||||
|
harness.completeChunk(10, 0, null, null);
|
||||||
|
harness.completeChunk(11, 0, null, new IllegalStateException("late failure"));
|
||||||
|
|
||||||
|
assertEquals(2, harness.failed.get());
|
||||||
|
assertEquals(0, harness.completed.get());
|
||||||
|
assertEquals(0, harness.inFlight.get());
|
||||||
|
assertEquals(2, harness.admission.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closingAbandonsRequestsThatNeverCompleteAndReturnsTheirPermits() throws Exception {
|
||||||
|
Harness harness = Harness.withOutstanding(2);
|
||||||
|
|
||||||
|
harness.abandon(new PregenAdmissionGate.Drain(false, false, 2));
|
||||||
|
|
||||||
|
assertEquals(2, harness.admission.availablePermits());
|
||||||
|
assertEquals(2, harness.failed.get());
|
||||||
|
assertEquals(0, harness.requests.size());
|
||||||
|
verify(harness.listener).onChunkFailed(10, 0);
|
||||||
|
verify(harness.listener).onChunkFailed(11, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Harness {
|
||||||
|
private final AsyncPregenMethod method;
|
||||||
|
private final PregenAdmissionGate admission;
|
||||||
|
private final PregenInFlightRequests requests;
|
||||||
|
private final PregenListener listener;
|
||||||
|
private final AtomicInteger inFlight;
|
||||||
|
private final AtomicLong failed;
|
||||||
|
private final AtomicLong completed;
|
||||||
|
|
||||||
|
private Harness(AsyncPregenMethod method, PregenAdmissionGate admission, PregenInFlightRequests requests,
|
||||||
|
PregenListener listener, AtomicInteger inFlight, AtomicLong failed, AtomicLong completed) {
|
||||||
|
this.method = method;
|
||||||
|
this.admission = admission;
|
||||||
|
this.requests = requests;
|
||||||
|
this.listener = listener;
|
||||||
|
this.inFlight = inFlight;
|
||||||
|
this.failed = failed;
|
||||||
|
this.completed = completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Harness withOutstanding(int outstanding) throws Exception {
|
||||||
|
AsyncPregenMethod method = mock(AsyncPregenMethod.class, CALLS_REAL_METHODS);
|
||||||
|
World world = mock(World.class);
|
||||||
|
when(world.getName()).thenReturn("qa");
|
||||||
|
PregenAdmissionGate admission = new PregenAdmissionGate(outstanding, 50L, System::currentTimeMillis);
|
||||||
|
PregenInFlightRequests requests = new PregenInFlightRequests();
|
||||||
|
PregenListener listener = mock(PregenListener.class);
|
||||||
|
AtomicInteger inFlight = new AtomicInteger(outstanding);
|
||||||
|
AtomicLong failed = new AtomicLong();
|
||||||
|
AtomicLong completed = new AtomicLong();
|
||||||
|
for (int index = 0; index < outstanding; index++) {
|
||||||
|
assertNotNull(admission.admit(() -> false, () -> false));
|
||||||
|
requests.add(PregenInFlightRequests.key(10 + index, 0), listener);
|
||||||
|
}
|
||||||
|
set(method, "world", world);
|
||||||
|
set(method, "admission", admission);
|
||||||
|
set(method, "backpressure", mock(PregenMantleBackpressure.class));
|
||||||
|
set(method, "mantleCleanup", mock(PregenSerialWorker.class));
|
||||||
|
set(method, "inFlightRequests", requests);
|
||||||
|
set(method, "generationFailure", new AtomicReference<>());
|
||||||
|
set(method, "regionPending", new ConcurrentHashMap<>());
|
||||||
|
set(method, "lastFailedReleaseLogAt", new AtomicLong());
|
||||||
|
set(method, "slowRequestWarnIntervalMs", 15_000);
|
||||||
|
set(method, "inFlight", inFlight);
|
||||||
|
set(method, "submitted", new AtomicLong(outstanding));
|
||||||
|
set(method, "completed", completed);
|
||||||
|
set(method, "failed", failed);
|
||||||
|
set(method, "lastProgressAt", new AtomicLong());
|
||||||
|
set(method, "adaptiveInFlightLimit", new AtomicInteger(outstanding));
|
||||||
|
set(method, "threads", outstanding);
|
||||||
|
set(method, "closing", new AtomicBoolean());
|
||||||
|
return new Harness(method, admission, requests, listener, inFlight, failed, completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void completeChunk(int x, int z, Chunk chunk, Throwable throwable) throws Exception {
|
||||||
|
Method complete = AsyncPregenMethod.class.getDeclaredMethod(
|
||||||
|
"completeChunk", int.class, int.class, PregenListener.class, Chunk.class, Throwable.class);
|
||||||
|
complete.setAccessible(true);
|
||||||
|
complete.invoke(method, x, z, listener, chunk, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void abandon(PregenAdmissionGate.Drain drain) throws Exception {
|
||||||
|
Method abandon = AsyncPregenMethod.class.getDeclaredMethod(
|
||||||
|
"abandonOutstandingRequests", PregenAdmissionGate.Drain.class);
|
||||||
|
abandon.setAccessible(true);
|
||||||
|
abandon.invoke(method, drain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void set(AsyncPregenMethod method, String name, Object value) throws Exception {
|
||||||
|
Field field = AsyncPregenMethod.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(method, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -148,9 +148,11 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
|||||||
AtomicInteger warnings = new AtomicInteger();
|
AtomicInteger warnings = new AtomicInteger();
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
try {
|
try {
|
||||||
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(
|
Future<PregenAdmissionGate.Drain> drain = executor.submit(() -> gate.awaitDrain(
|
||||||
10L,
|
10L,
|
||||||
TimeUnit.MILLISECONDS,
|
TimeUnit.MILLISECONDS,
|
||||||
|
5L,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
warnings::incrementAndGet
|
warnings::incrementAndGet
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -160,7 +162,9 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
|||||||
gate.release();
|
gate.release();
|
||||||
gate.release();
|
gate.release();
|
||||||
|
|
||||||
assertFalse(drain.get(1L, TimeUnit.SECONDS));
|
PregenAdmissionGate.Drain result = drain.get(1L, TimeUnit.SECONDS);
|
||||||
|
assertTrue(result.drained());
|
||||||
|
assertFalse(result.interrupted());
|
||||||
assertTrue(warnings.get() > 0);
|
assertTrue(warnings.get() > 0);
|
||||||
assertEquals(2, gate.availablePermits());
|
assertEquals(2, gate.availablePermits());
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+2
@@ -514,6 +514,8 @@ public class AsyncPregenServerSaveOwnershipTest {
|
|||||||
set("regionPending", pendingRegions);
|
set("regionPending", pendingRegions);
|
||||||
set("pendingEvictions", evictions);
|
set("pendingEvictions", evictions);
|
||||||
set("chunkIoExecutor", (Executor) Runnable::run);
|
set("chunkIoExecutor", (Executor) Runnable::run);
|
||||||
|
set("generationFailure", new AtomicReference<>());
|
||||||
|
set("inFlightRequests", new PregenInFlightRequests());
|
||||||
when(chunk.getX()).thenReturn(2);
|
when(chunk.getX()).thenReturn(2);
|
||||||
when(chunk.getZ()).thenReturn(3);
|
when(chunk.getZ()).thenReturn(3);
|
||||||
when(binding.saveAndUnloadChunk(world, 2, 3)).thenReturn(true);
|
when(binding.saveAndUnloadChunk(world, 2, 3)).thenReturn(true);
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ public class AsyncPregenWorkerBoostTest {
|
|||||||
AsyncPregenMethod method = mock(AsyncPregenMethod.class, CALLS_REAL_METHODS);
|
AsyncPregenMethod method = mock(AsyncPregenMethod.class, CALLS_REAL_METHODS);
|
||||||
PregenAdmissionGate admission = mock(PregenAdmissionGate.class);
|
PregenAdmissionGate admission = mock(PregenAdmissionGate.class);
|
||||||
IllegalStateException failure = new IllegalStateException("drain failed");
|
IllegalStateException failure = new IllegalStateException("drain failed");
|
||||||
when(admission.awaitDrain(anyLong(), any(TimeUnit.class), any(Runnable.class))).thenThrow(failure);
|
when(admission.awaitDrain(anyLong(), any(TimeUnit.class), anyLong(), any(TimeUnit.class), any(Runnable.class)))
|
||||||
|
.thenThrow(failure);
|
||||||
set(method, "closing", new AtomicBoolean());
|
set(method, "closing", new AtomicBoolean());
|
||||||
set(method, "holdsWorkerBoost", new AtomicBoolean(true));
|
set(method, "holdsWorkerBoost", new AtomicBoolean(true));
|
||||||
set(method, "admission", admission);
|
set(method, "admission", admission);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -162,7 +163,8 @@ public class PregenAdmissionGateTest {
|
|||||||
AtomicInteger warnings = new AtomicInteger();
|
AtomicInteger warnings = new AtomicInteger();
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
try {
|
try {
|
||||||
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(5L, TimeUnit.MILLISECONDS, warnings::incrementAndGet));
|
Future<PregenAdmissionGate.Drain> drain = executor.submit(() -> gate.awaitDrain(
|
||||||
|
5L, TimeUnit.MILLISECONDS, 5L, TimeUnit.SECONDS, warnings::incrementAndGet));
|
||||||
|
|
||||||
while (warnings.get() < 2) {
|
while (warnings.get() < 2) {
|
||||||
Thread.onSpinWait();
|
Thread.onSpinWait();
|
||||||
@@ -170,13 +172,46 @@ public class PregenAdmissionGateTest {
|
|||||||
gate.release();
|
gate.release();
|
||||||
gate.release();
|
gate.release();
|
||||||
|
|
||||||
assertEquals(Boolean.FALSE, drain.get(5, TimeUnit.SECONDS));
|
PregenAdmissionGate.Drain result = drain.get(5, TimeUnit.SECONDS);
|
||||||
|
assertTrue(result.drained());
|
||||||
|
assertFalse(result.interrupted());
|
||||||
|
assertEquals(0, result.outstanding());
|
||||||
assertEquals(2, gate.availablePermits());
|
assertEquals(2, gate.availablePermits());
|
||||||
} finally {
|
} finally {
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drainGivesUpAtItsDeadlineWhilePermitsStayHeld() throws Exception {
|
||||||
|
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||||
|
assertNotNull(gate.admit(() -> false, () -> false));
|
||||||
|
assertNotNull(gate.admit(() -> false, () -> false));
|
||||||
|
AtomicInteger warnings = new AtomicInteger();
|
||||||
|
|
||||||
|
PregenAdmissionGate.Drain result = gate.awaitDrain(
|
||||||
|
5L, TimeUnit.MILLISECONDS, 80L, TimeUnit.MILLISECONDS, warnings::incrementAndGet);
|
||||||
|
|
||||||
|
assertFalse(result.drained());
|
||||||
|
assertFalse(result.interrupted());
|
||||||
|
assertEquals(2, result.outstanding());
|
||||||
|
assertTrue(warnings.get() > 0);
|
||||||
|
assertEquals(0, gate.availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drainReturnsAtOnceWhenNothingIsHeld() {
|
||||||
|
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||||
|
AtomicInteger warnings = new AtomicInteger();
|
||||||
|
|
||||||
|
PregenAdmissionGate.Drain result = gate.awaitDrain(
|
||||||
|
5L, TimeUnit.MILLISECONDS, 5L, TimeUnit.SECONDS, warnings::incrementAndGet);
|
||||||
|
|
||||||
|
assertTrue(result.drained());
|
||||||
|
assertEquals(0, result.outstanding());
|
||||||
|
assertEquals(0, warnings.get());
|
||||||
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException.class)
|
@Test(expected = IllegalArgumentException.class)
|
||||||
public void aGateWithoutPermitsIsRejected() {
|
public void aGateWithoutPermitsIsRejected() {
|
||||||
new PregenAdmissionGate(0, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
new PregenAdmissionGate(0, GENEROUS_BOUND_MS, System::currentTimeMillis);
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package art.arcane.iris.world.pregen;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class PregenInFlightRequestsTest {
|
||||||
|
@Test
|
||||||
|
public void settlingARequestReturnsItsListenerOnlyOnce() {
|
||||||
|
PregenInFlightRequests requests = new PregenInFlightRequests();
|
||||||
|
PregenListener listener = mock(PregenListener.class);
|
||||||
|
long key = PregenInFlightRequests.key(48, -4);
|
||||||
|
|
||||||
|
assertTrue(requests.add(key, listener));
|
||||||
|
assertEquals(1, requests.size());
|
||||||
|
assertSame(listener, requests.settle(key));
|
||||||
|
assertNull(requests.settle(key));
|
||||||
|
assertEquals(0, requests.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aDuplicateSubmissionKeepsTheOriginalRequest() {
|
||||||
|
PregenInFlightRequests requests = new PregenInFlightRequests();
|
||||||
|
PregenListener first = mock(PregenListener.class);
|
||||||
|
PregenListener second = mock(PregenListener.class);
|
||||||
|
long key = PregenInFlightRequests.key(1, 2);
|
||||||
|
|
||||||
|
assertTrue(requests.add(key, first));
|
||||||
|
assertFalse(requests.add(key, second));
|
||||||
|
assertSame(first, requests.settle(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drainingHandsBackEveryOutstandingRequestExactlyOnce() {
|
||||||
|
PregenInFlightRequests requests = new PregenInFlightRequests();
|
||||||
|
PregenListener listener = mock(PregenListener.class);
|
||||||
|
requests.add(PregenInFlightRequests.key(0, 0), listener);
|
||||||
|
requests.add(PregenInFlightRequests.key(1, 0), listener);
|
||||||
|
requests.add(PregenInFlightRequests.key(-7, 9), listener);
|
||||||
|
|
||||||
|
Map<Long, PregenListener> drained = requests.drain();
|
||||||
|
|
||||||
|
assertEquals(3, drained.size());
|
||||||
|
assertEquals(0, requests.size());
|
||||||
|
assertTrue(requests.drain().isEmpty());
|
||||||
|
assertNull(requests.settle(PregenInFlightRequests.key(1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void keysDistinguishNegativeCoordinates() {
|
||||||
|
assertTrue(PregenInFlightRequests.key(1, -1) != PregenInFlightRequests.key(-1, 1));
|
||||||
|
assertTrue(PregenInFlightRequests.key(0, -1) != PregenInFlightRequests.key(-1, 0));
|
||||||
|
assertEquals(PregenInFlightRequests.key(48, -4), PregenInFlightRequests.key(48, -4));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,14 @@ tasks.named('processResources').configure {
|
|||||||
dependsOn(unpackNativeMinecraftSources)
|
dependsOn(unpackNativeMinecraftSources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('verifyLoggingPolicy').configure {
|
||||||
|
dependsOn(unpackNativeMinecraftSources)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('verifyNativeClassBoundary').configure {
|
||||||
|
dependsOn(tasks.named('classes'))
|
||||||
|
}
|
||||||
|
|
||||||
tasks.matching { Task task -> task.name == 'createMinecraftArtifacts' }.configureEach {
|
tasks.matching { Task task -> task.name == 'createMinecraftArtifacts' }.configureEach {
|
||||||
dependsOn(unpackNativeMinecraftSources)
|
dependsOn(unpackNativeMinecraftSources)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user