mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Improve generation and lifecycle safety
- Reduce terrain, noise, hydrology, and decorator sampling overhead. - Reduce object transaction allocations while preserving rollback behavior. - Optimize structure placement and cave anchor sampling. - Parallelize Studio cache warmup and protect world entry and hotload recovery. - Validate pack fingerprints and publish pack replacements atomically. - Notify modded operators about outdated packs without synchronizing pack data. - Harden world creation, loading, evacuation, and teleport failure recovery. - Coalesce queued region flushes while preserving chunk save ordering. - Remove obsolete mantle, pack, and cave planning code. - Add regression tests and reusable gameplay fixtures.
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
@@ -174,7 +176,6 @@ public class CommandPack implements DirectorExecutor {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||
return;
|
||||
@@ -230,7 +231,6 @@ public class CommandPack implements DirectorExecutor {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
||||
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||
return;
|
||||
@@ -363,47 +363,79 @@ public class CommandPack implements DirectorExecutor {
|
||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||
}
|
||||
|
||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
PackValidationRegistry.ValidationTicket ticket =
|
||||
PackValidationRegistry.tryBeginValidation(packFolder.toPath());
|
||||
PackValidationResult result;
|
||||
String contentFingerprint = "";
|
||||
String contextFingerprint = "";
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packFolder);
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
if (ticket == null) {
|
||||
throw new IllegalStateException("Pack content is currently being changed.");
|
||||
}
|
||||
contentFingerprint = PackFingerprints.computePackTreeFingerprint(packFolder);
|
||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
result = PackValidator.validate(packFolder);
|
||||
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packFolder))
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
throw new IllegalStateException("Pack content or validation context changed during validation.");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||
? e.getClass().getSimpleName()
|
||||
: e.getMessage();
|
||||
PackValidationResult result = new PackValidationResult(
|
||||
result = new PackValidationResult(
|
||||
packFolder.getName(),
|
||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
contentFingerprint = "";
|
||||
contextFingerprint = "";
|
||||
}
|
||||
if (PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||
PackValidationRegistry.publish(result);
|
||||
} else {
|
||||
result = new PackValidationResult(packFolder.getName(),
|
||||
List.of("Pack content changed during validation."), List.of(), System.currentTimeMillis());
|
||||
}
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistValidationCache(File packsRoot) {
|
||||
try {
|
||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
PackFingerprints.PackContentSnapshot snapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||
for (File packDirectory : packDirectories) {
|
||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
||||
PackValidationResult result = PackValidationRegistry.getMatching(
|
||||
packDirectory.toPath(), snapshot.packContents().get(packDirectory.getName()));
|
||||
if (result == null) {
|
||||
// The boot cache only loads when it covers every pack, so a partial write is
|
||||
// pointless - but say so instead of silently skipping the persist forever.
|
||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
||||
+ "\" has no validation matching its current content. Run /iris pack validate to refresh it.");
|
||||
return;
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
try {
|
||||
if (!snapshot.content().equals(PackFingerprints.computePackContentSnapshot(packsRoot).content())
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
Iris.warn("Pack validation cache not written because pack content or validation context changed.");
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < packDirectories.size(); index++) {
|
||||
File packDirectory = packDirectories.get(index);
|
||||
if (PackValidationRegistry.getMatching(packDirectory.toPath(),
|
||||
snapshot.packContents().get(packDirectory.getName())) != results.get(index)) {
|
||||
Iris.warn("Pack validation cache not written because validation changed during persistence.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
PackValidationCache.save(
|
||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||
PackValidationCache.contentFingerprint(packsRoot),
|
||||
PackValidationCache.contextFingerprint(),
|
||||
snapshot.content(),
|
||||
contextFingerprint,
|
||||
results);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
||||
|
||||
+11
-10
@@ -18,8 +18,9 @@
|
||||
|
||||
package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
||||
@@ -184,12 +185,12 @@ public final class IrisWorldGeneratorResolver {
|
||||
}
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
Path cacheFile = IrisPlatforms.get().dataFile("cache", "pack-validation.json").toPath();
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot =
|
||||
new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
PackFingerprints.PackContentSnapshot contentSnapshot =
|
||||
new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||
String contextFingerprint = "";
|
||||
Optional<List<PackValidationResult>> cached = Optional.empty();
|
||||
try {
|
||||
contentSnapshot = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
contentSnapshot = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
if (!externalContentChanged) {
|
||||
cached = PackValidationCache.load(
|
||||
@@ -274,19 +275,19 @@ public final class IrisWorldGeneratorResolver {
|
||||
private static FreshValidation validateStablePacks(
|
||||
File packsRoot,
|
||||
List<File> initialPackDirs,
|
||||
ServerConfigurator.PackContentSnapshot initialSnapshot
|
||||
PackFingerprints.PackContentSnapshot initialSnapshot
|
||||
) {
|
||||
List<File> packDirs = initialPackDirs;
|
||||
ServerConfigurator.PackContentSnapshot before = initialSnapshot;
|
||||
PackFingerprints.PackContentSnapshot before = initialSnapshot;
|
||||
for (int attempt = 0; attempt < VALIDATION_STABILITY_ATTEMPTS; attempt++) {
|
||||
List<String> packNames = packDirs.stream().map(File::getName).sorted().toList();
|
||||
List<PackValidationResult> results = validatePacks(packDirs);
|
||||
ServerConfigurator.PackContentSnapshot after;
|
||||
PackFingerprints.PackContentSnapshot after;
|
||||
try {
|
||||
after = ServerConfigurator.computePackContentSnapshot(packsRoot);
|
||||
after = PackFingerprints.computePackContentSnapshot(packsRoot);
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Could not verify Iris pack bytes after validation", exception);
|
||||
after = new ServerConfigurator.PackContentSnapshot("", Map.of());
|
||||
after = new PackFingerprints.PackContentSnapshot("", Map.of());
|
||||
}
|
||||
List<File> afterPackDirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<String> afterPackNames = afterPackDirs.stream().map(File::getName).sorted().toList();
|
||||
@@ -923,7 +924,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
|
||||
private record FreshValidation(
|
||||
List<File> packDirs,
|
||||
ServerConfigurator.PackContentSnapshot contentSnapshot,
|
||||
PackFingerprints.PackContentSnapshot contentSnapshot,
|
||||
List<PackValidationResult> results,
|
||||
boolean stable
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:oak_stairs",
|
||||
"data": {
|
||||
"half": "bottom",
|
||||
"facing": "north"
|
||||
}
|
||||
}
|
||||
],
|
||||
"chance": 1,
|
||||
"forcePlace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Decorator Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-decorators',
|
||||
description: 'Verify authored stair decorations occupy one block and retain their half state.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Decorator generation requires an isolated instance')
|
||||
await context.step('create a world with authored stair decorations', async () => {
|
||||
const response = await context.command('/iris create name=decorator_probe type=decorator-fixture seed=78264193',
|
||||
/Successfully created your world|Failed to create/, 180000)
|
||||
context.expect(response.includes('Successfully created your world'), response)
|
||||
})
|
||||
await context.step('client sees single bottom-half stair decorations', async () => {
|
||||
let checked = 0
|
||||
await context.waitUntil(() => {
|
||||
checked = 0
|
||||
const center = context.bot.entity.position.floored()
|
||||
for (let x = -6; x <= 6; x++) {
|
||||
for (let z = -6; z <= 6; z++) {
|
||||
for (let y = -4; y <= 1; y++) {
|
||||
const position = center.offset(x, y, z)
|
||||
if (context.bot.blockAt(position)?.name !== 'lime_wool') continue
|
||||
const decoration = context.bot.blockAt(position.offset(0, 1, 0))
|
||||
const above = context.bot.blockAt(position.offset(0, 2, 0))
|
||||
if (!decoration || !above) return false
|
||||
if (decoration.name !== 'oak_stairs' || decoration.getProperties().half !== 'bottom'
|
||||
|| above.name !== 'air') return false
|
||||
checked++
|
||||
}
|
||||
}
|
||||
}
|
||||
return checked >= 100
|
||||
}, { label: 'At least 100 authored stair columns with empty space above', timeoutMs: 60000, intervalMs: 250 })
|
||||
context.expect(checked >= 100, 'Client verified generated decorator columns', { checked })
|
||||
context.report.decoratorColumns = checked
|
||||
})
|
||||
}
|
||||
}
|
||||
+71
-1
@@ -4,6 +4,13 @@ import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.WorldCreatorCompat;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Biome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
||||
@@ -19,6 +26,10 @@ import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
@@ -26,6 +37,7 @@ import java.util.logging.Level;
|
||||
public final class NativeLifecycleFixture extends JavaPlugin implements WorldRuntimeExecution {
|
||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
||||
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
|
||||
private static final long WORLD_SEED = 78264193L;
|
||||
private NativeWorldRuntime runtime;
|
||||
|
||||
@@ -45,7 +57,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload");
|
||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
@@ -56,6 +68,8 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
case "terrain-create" -> createTerrain(sender);
|
||||
case "terrain-check" -> checkTerrain(sender);
|
||||
case "terrain-unload" -> unloadTerrain(sender);
|
||||
case "lifecycle-status" -> lifecycleStatus(sender);
|
||||
case "lifecycle-entry" -> lifecycleEntry(sender);
|
||||
case "status" -> sender.sendMessage("NATIVE_STATUS available=" + runtime.available()
|
||||
+ " loaded=" + (Bukkit.getWorld(WORLD_KEY) != null));
|
||||
default -> throw new IllegalArgumentException("Unknown lifecycle operation");
|
||||
@@ -66,6 +80,61 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
return true;
|
||||
}
|
||||
|
||||
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||
Path levelRoot = IrisWorldStorage.levelRoot().toPath();
|
||||
Path registry = levelRoot.resolve("iris/worlds.json");
|
||||
boolean registered = Files.exists(registry)
|
||||
&& Files.readString(registry).contains("\"" + COMMAND_KEY + "\"");
|
||||
String startupName = IrisWorldStorage.configuredWorldName(COMMAND_KEY, levelRoot.getFileName().toString());
|
||||
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
boolean configured = configuration.getConfigurationSection("worlds") != null
|
||||
&& configuration.getConfigurationSection("worlds").contains(startupName);
|
||||
File quarantineRoot = levelRoot.resolve("dimensions/iris").toFile();
|
||||
File[] quarantines = quarantineRoot.listFiles((directory, name) -> name.startsWith(".iris-delete-"));
|
||||
int quarantineCount = quarantines == null ? 0 : quarantines.length;
|
||||
Path queueFile = Bukkit.getPluginManager().getPlugin("Iris").getDataFolder().toPath()
|
||||
.resolve("pending-world-deletes.txt");
|
||||
List<String> queue = Files.exists(queueFile) ? Files.readAllLines(queueFile) : List.of();
|
||||
boolean queued = quarantineCount > 0;
|
||||
if (quarantines != null) {
|
||||
for (File quarantine : quarantines) {
|
||||
queued &= queue.contains(quarantine.getName());
|
||||
}
|
||||
}
|
||||
boolean disk = Files.exists(storage, LinkOption.NOFOLLOW_LINKS);
|
||||
String cleanup = disk ? "present" : quarantineCount == 0 ? "deleted" : queued ? "queued" : "unsafe";
|
||||
boolean idle = true;
|
||||
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
|
||||
for (LifecycleOperationCoordinator.Domain domain : LifecycleOperationCoordinator.Domain.values()) {
|
||||
idle &= coordinator.active(domain).isEmpty();
|
||||
}
|
||||
sender.sendMessage("IRIS_LIFECYCLE loaded=" + (world != null) + " registered=" + registered
|
||||
+ " configured=" + configured + " disk=" + disk + " quarantines=" + quarantineCount
|
||||
+ " cleanup=" + cleanup + " idle=" + idle);
|
||||
}
|
||||
|
||||
private void lifecycleEntry(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalArgumentException("Lifecycle entry requires a player");
|
||||
}
|
||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||
if (world == null || player.getWorld() != world || world.getSeed() != WORLD_SEED
|
||||
|| !IrisToolbelt.isIrisWorld(world)) {
|
||||
throw new IllegalStateException("Player did not enter the requested seeded Iris world");
|
||||
}
|
||||
Location feet = player.getLocation();
|
||||
Material floor = world.getBlockAt(feet.getBlockX(), feet.getBlockY() - 1, feet.getBlockZ()).getType();
|
||||
if (floor != Material.LIME_WOOL || !floor.isSolid()
|
||||
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY(), feet.getBlockZ()).isPassable()
|
||||
|| !world.getBlockAt(feet.getBlockX(), feet.getBlockY() + 1, feet.getBlockZ()).isPassable()) {
|
||||
throw new IllegalStateException("Lifecycle player entry lacks a safe fixture floor");
|
||||
}
|
||||
sender.sendMessage("IRIS_LIFECYCLE_ENTRY world=" + COMMAND_KEY + " seed=" + world.getSeed()
|
||||
+ " floor=lime_wool safe=true");
|
||||
}
|
||||
|
||||
private void create(CommandSender sender) throws ReflectiveOperationException {
|
||||
if (!runtime.available() || Bukkit.getWorld(WORLD_KEY) != null) {
|
||||
throw new IllegalStateException("Native runtime unavailable or probe world already loaded");
|
||||
@@ -124,6 +193,7 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
int y = world.getHighestBlockYAt(x, z);
|
||||
if (y < 90 || y > 100 || world.getBlockAt(x, y, z).getType() != Material.LIME_WOOL
|
||||
|| world.getBlockAt(x, y - 3, z).getType() != Material.GREEN_CONCRETE
|
||||
|| world.getBlockAt(x, world.getMinHeight(), z).getType() != Material.GREEN_CONCRETE
|
||||
|| world.getBiome(x, y, z) != Biome.PLAINS) {
|
||||
throw new IllegalStateException("Terrain column differs at " + x + "," + y + "," + z
|
||||
+ ": " + world.getBlockAt(x, y, z).getType());
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 96
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "Noise Sources",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"expression": "0.5 + sin(x * 0.15) * 0.25 + cos(y * 0.13) * 0.25"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"expression": "terrain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"seed": 311,
|
||||
"style": {
|
||||
"style": "CLOVER_HERMITE",
|
||||
"zoom": 0.1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-noise',
|
||||
description: 'Verify expression and interpolated noise produce varied, bounded terrain visible to a player.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Noise generation requires an isolated instance')
|
||||
await context.step('create a world with expression and Clover noise', async () => {
|
||||
const response = await context.command('/iris create name=noise_probe type=noise-fixture seed=78264193',
|
||||
/Successfully created your world|Failed to create/, 180000)
|
||||
context.expect(response.includes('Successfully created your world'), response)
|
||||
})
|
||||
await context.step('client observes bounded terrain with varying heights', async () => {
|
||||
let columns = 0
|
||||
let minimum = Infinity
|
||||
let maximum = -Infinity
|
||||
await context.waitUntil(() => {
|
||||
columns = 0
|
||||
minimum = Infinity
|
||||
maximum = -Infinity
|
||||
const center = context.bot.entity.position.floored()
|
||||
for (let x = -8; x <= 8; x++) {
|
||||
for (let z = -8; z <= 8; z++) {
|
||||
let surface = null
|
||||
for (let y = 60; y <= 100; y++) {
|
||||
const position = center.offset(x, y - center.y, z)
|
||||
const block = context.bot.blockAt(position)
|
||||
if (!block) return false
|
||||
if (block.name === 'lime_wool') {
|
||||
if (surface !== null || context.bot.blockAt(position.offset(0, 1, 0))?.name !== 'air') return false
|
||||
surface = y
|
||||
}
|
||||
}
|
||||
if (surface === null || surface < 64 || surface > 96) return false
|
||||
columns++
|
||||
minimum = Math.min(minimum, surface)
|
||||
maximum = Math.max(maximum, surface)
|
||||
}
|
||||
}
|
||||
return columns === 289 && maximum - minimum >= 3
|
||||
}, { label: '289 noise columns with bounded, varied surfaces', timeoutMs: 60000, intervalMs: 250 })
|
||||
context.expect(columns === 289 && maximum - minimum >= 3, 'Client verified noise terrain', { columns, minimum, maximum })
|
||||
context.report.noiseTerrain = { columns, minimum, maximum }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 64,
|
||||
"max": 64
|
||||
}
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"place": [
|
||||
"object"
|
||||
],
|
||||
"chance": 1,
|
||||
"density": 4,
|
||||
"mode": "CENTER_HEIGHT",
|
||||
"rotation": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Structure Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:stone"
|
||||
}
|
||||
]
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"structures": [
|
||||
"surface"
|
||||
],
|
||||
"placementId": "surface",
|
||||
"distribution": "DENSITY",
|
||||
"density": 1,
|
||||
"anchor": "SURFACE",
|
||||
"terrain": {
|
||||
"mode": "PRESERVE"
|
||||
},
|
||||
"stilt": {
|
||||
"maxDepth": 16,
|
||||
"palette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:diamond_block"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"structures": [
|
||||
"buried"
|
||||
],
|
||||
"placementId": "buried",
|
||||
"distribution": "DENSITY",
|
||||
"density": 1,
|
||||
"anchor": "HEIGHT_BAND",
|
||||
"minHeight": 24,
|
||||
"maxHeight": 24,
|
||||
"terrain": {
|
||||
"mode": "BORE"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"object": "buried",
|
||||
"connectors": [],
|
||||
"rotatable": false,
|
||||
"rules": {
|
||||
"terminal": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"object": "surface",
|
||||
"connectors": [],
|
||||
"rotatable": false,
|
||||
"rules": {
|
||||
"terminal": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "buried",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pieces": [
|
||||
{
|
||||
"piece": "surface",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"startPool": "buried",
|
||||
"maxDepth": 1,
|
||||
"maxSizeChunks": 1,
|
||||
"placeMode": "STRUCTURE_PIECE"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"startPool": "surface",
|
||||
"maxDepth": 1,
|
||||
"maxSizeChunks": 1,
|
||||
"placeMode": "STRUCTURE_PIECE"
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-structures',
|
||||
description: 'Verify surface foundations, buried rooms, and waterlogged object blocks in generated chunks.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Structure generation requires an isolated instance')
|
||||
const world = context.options.command ?? 'structure_probe'
|
||||
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
|
||||
await context.step('create the structure fixture world', async () => {
|
||||
const response = await context.command(`/iris create name=${world} type=structure-fixture seed=78264193`,
|
||||
/World ready|Failed to create/, 180000)
|
||||
context.expect(response.includes('World ready'), response)
|
||||
})
|
||||
await context.step('observe structures and objects above and below terrain', async () => {
|
||||
await context.waitUntil(() => {
|
||||
if (Math.abs(context.bot.entity.position.x) > 32 || Math.abs(context.bot.entity.position.z) > 32) return false
|
||||
for (let x = -16; x < 32; x += 16) {
|
||||
for (let z = -16; z < 32; z += 16) {
|
||||
for (let y = 16; y <= 80; y += 16) {
|
||||
if (!context.bot.blockAt(context.bot.entity.position.clone().set(x, y, z))) return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, { label: 'Complete structure fixture chunks', timeoutMs: 60000, intervalMs: 100 })
|
||||
const digest = createHash('sha256')
|
||||
const counts = { emerald_block: 0, diamond_block: 0, blue_concrete: 0, gold_block: 0, oak_slab: 0 }
|
||||
for (let x = -16; x < 32; x++) {
|
||||
for (let z = -16; z < 32; z++) {
|
||||
for (let y = 16; y <= 80; y++) {
|
||||
const block = context.bot.blockAt(context.bot.entity.position.clone().set(x, y, z))
|
||||
context.expect(block !== null, 'Every sampled structure block must be loaded')
|
||||
digest.update(`${block.stateId}\n`)
|
||||
if (Object.hasOwn(counts, block.name)) counts[block.name]++
|
||||
if (block.name === 'emerald_block') context.expect(y > 64, 'Surface platform must be above terrain')
|
||||
if (block.name === 'blue_concrete') context.expect(y < 40, 'Buried room must remain underground')
|
||||
if (block.name === 'oak_slab') context.expect(block.getProperties().waterlogged === false,
|
||||
'Object slabs placed on dry ground must clear waterlogging')
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [block, count] of Object.entries(counts)) {
|
||||
context.expect(count > 0, `Generated fixture must contain ${block}`, { count })
|
||||
}
|
||||
context.report.structures = { counts, hash: digest.digest('hex') }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { readFile, stat } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-studio-open',
|
||||
description: 'Measure repeated Studio entry and verify visible terrain, spectator mode, and world cleanup.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Studio entry requires an isolated instance')
|
||||
const match = /^([a-z0-9_-]+):([1-4])$/.exec(context.options.command ?? 'overworld:2')
|
||||
context.expect(match, 'Expected pack:cycles with one to four cycles')
|
||||
context.report.studioOpen = []
|
||||
for (let cycle = 0; cycle < Number(match[2]); cycle++) {
|
||||
await context.step(`open and close Studio ${cycle + 1}`, async () => {
|
||||
const before = new Set((await context.observe()).worlds.map(world => world.id))
|
||||
const start = Date.now()
|
||||
let world
|
||||
let center
|
||||
await context.command(`/iris studio open ${match[1]} seed=1337`)
|
||||
await context.waitUntil(async () => {
|
||||
const observation = await context.observe()
|
||||
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||
world = observation.worlds.find(entry => entry.id === player?.world)
|
||||
if (!world || before.has(world.id) || !/iris-[0-9a-f-]{36}$/.test(world.name)) return false
|
||||
center = context.bot.entity.position.floored()
|
||||
return world.loadedChunks > 0 && context.bot.blockAt(center) !== null && context.bot.game.gameMode === 'spectator'
|
||||
}, { label: 'Studio arrival with terrain and spectator mode', timeoutMs: 180000, intervalMs: 100 })
|
||||
const arrivalMs = Date.now() - start
|
||||
context.expect(Number.isInteger(context.bot.game.minY) && Number.isInteger(context.bot.game.height),
|
||||
'Studio dimension must report its vertical range')
|
||||
await context.waitUntil(() => {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let y = context.bot.game.minY; y < context.bot.game.minY + context.bot.game.height; y += 16) {
|
||||
if (context.bot.blockAt(center.offset(x, y - center.y, z)) === null) return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, { label: 'Complete neighboring Studio columns', timeoutMs: 60000, intervalMs: 100 })
|
||||
const digest = createHash('sha256')
|
||||
let solid = 0
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let y = context.bot.game.minY; y < context.bot.game.minY + context.bot.game.height; y++) {
|
||||
const block = context.bot.blockAt(center.offset(x, y - center.y, z))
|
||||
context.expect(block !== null, 'Studio entry column must be available')
|
||||
digest.update(`${block.name}\n`)
|
||||
if (!['air', 'cave_air', 'void_air', 'water', 'lava'].includes(block.name)) solid++
|
||||
}
|
||||
}
|
||||
}
|
||||
context.expect(solid > 0, 'Studio entry must contain solid terrain')
|
||||
context.report.studioOpen.push({ cycle, arrivalMs, world: world.name, x: center.x, z: center.z,
|
||||
terrainHash: digest.digest('hex'), solid })
|
||||
const closed = await context.command('/iris studio close', /^Studio closed\.$|Studio close failed:/, 180000)
|
||||
context.expect(closed === 'Studio closed.', closed)
|
||||
await context.waitUntil(async () => !(await context.observe()).worlds.some(entry => entry.id === world.id),
|
||||
{ label: 'Studio world unload', timeoutMs: 90000, intervalMs: 100 })
|
||||
const transientName = /iris-[0-9a-f-]{36}$/.exec(world.name)[0]
|
||||
const properties = await readFile(path.join(context.server.directory, 'server.properties'), 'utf8')
|
||||
const level = /^level-name=(.+)$/m.exec(properties)?.[1].trim() ?? 'world'
|
||||
const directory = path.join(context.server.directory, level, 'dimensions', 'iris', transientName)
|
||||
await context.waitUntil(async () => {
|
||||
try { await stat(directory); return false } catch (error) {
|
||||
if (error.code === 'ENOENT') return true
|
||||
throw error
|
||||
}
|
||||
}, { label: 'Studio directory cleanup', timeoutMs: 90000, intervalMs: 100 })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const worldName = 'lifecycle_probe'
|
||||
const worldKey = `iris:${worldName}`
|
||||
const storageName = `world_iris_${worldName}`
|
||||
|
||||
export default {
|
||||
name: 'iris-world-lifecycle',
|
||||
description: 'Create, enter, evacuate, reload, and remove an Iris world through the player commands.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'World lifecycle requires an isolated instance')
|
||||
const wait = (predicate, label, timeoutMs = 180000) => context.waitUntil(predicate, {
|
||||
label, timeoutMs, intervalMs: 250
|
||||
})
|
||||
const playerWorld = async () => {
|
||||
const observation = await context.observe()
|
||||
const player = observation.players.find(entry => entry.username === context.bot.username)
|
||||
return observation.worlds.find(entry => entry.id === player?.world)?.name
|
||||
}
|
||||
const status = async () => {
|
||||
const response = await context.command('/irislifecycle lifecycle-status', /IRIS_LIFECYCLE |NATIVE_FAILED/, 10000)
|
||||
context.expect(!response.includes('NATIVE_FAILED'), response)
|
||||
return response
|
||||
}
|
||||
const settledStatus = async expected => {
|
||||
const response = await status()
|
||||
return response.includes(expected) && response.includes('idle=true')
|
||||
}
|
||||
const assertEvacuated = async label => {
|
||||
let destination
|
||||
await wait(async () => {
|
||||
destination = await playerWorld()
|
||||
return Boolean(destination && destination !== storageName)
|
||||
}, label, 30000)
|
||||
context.expect(destination && destination !== storageName, label, { destination })
|
||||
}
|
||||
const assertSafeEntry = async () => {
|
||||
await wait(async () => (await playerWorld()) === storageName, 'Player entered the lifecycle world')
|
||||
await wait(() => {
|
||||
const feet = context.bot.entity.position.floored()
|
||||
const floor = context.bot.blockAt(feet.offset(0, -1, 0))
|
||||
const body = context.bot.blockAt(feet)
|
||||
const head = context.bot.blockAt(feet.offset(0, 1, 0))
|
||||
return context.bot.entity.onGround && floor?.name === 'lime_wool'
|
||||
&& body?.boundingBox === 'empty' && head?.boundingBox === 'empty'
|
||||
}, 'Client sees solid terrain below unobstructed player entry', 30000)
|
||||
const response = await context.command('/irislifecycle lifecycle-entry', /IRIS_LIFECYCLE_ENTRY|NATIVE_FAILED/, 10000)
|
||||
context.expect(response.includes(`IRIS_LIFECYCLE_ENTRY world=${worldKey} seed=78264193 floor=lime_wool safe=true`), response)
|
||||
}
|
||||
await context.step('confirm disposable world is absent', async () => {
|
||||
const response = await status()
|
||||
context.expect(response.includes('loaded=false registered=false configured=false disk=false quarantines=0 cleanup=deleted idle=true'), response)
|
||||
await context.command('/minecraft:gamemode survival @s')
|
||||
await wait(() => context.bot.game.gameMode === 'survival', 'Survival mode', 10000)
|
||||
})
|
||||
await context.step('create through Iris and automatically enter safe terrain', async () => {
|
||||
await context.command(`/iris create name=${worldName} type=native-terrain seed=78264193`)
|
||||
await wait(async () => await settledStatus('loaded=true registered=true configured=true disk=true'), 'Created world registration and storage')
|
||||
await assertSafeEntry()
|
||||
})
|
||||
await context.step('unload while the player is inside and verify evacuation', async () => {
|
||||
context.expect((await playerWorld()) === storageName, 'Player must be inside the world before unload')
|
||||
await context.command(`/iris unloadWorld ${worldKey}`)
|
||||
await wait(async () => await settledStatus('loaded=false registered=true configured=true disk=true'), 'World unloaded with storage preserved')
|
||||
await assertEvacuated('Unload evacuated the player to a loaded world')
|
||||
})
|
||||
await context.step('reload the saved world and enter it again', async () => {
|
||||
await context.command(`/iris loadWorld ${worldName}`)
|
||||
await wait(async () => await settledStatus('loaded=true registered=true configured=true disk=true'), 'World reloaded')
|
||||
await context.command(`/iris tp ${worldKey}`)
|
||||
await assertSafeEntry()
|
||||
})
|
||||
await context.step('remove the occupied world and verify disk cleanup', async () => {
|
||||
await context.command(`/iris remove world=${worldName} delete=true`)
|
||||
let finalStatus
|
||||
await wait(async () => {
|
||||
finalStatus = await status()
|
||||
return finalStatus.includes('loaded=false registered=false configured=false disk=false')
|
||||
&& /cleanup=(deleted|queued)\b/.test(finalStatus) && finalStatus.includes('idle=true')
|
||||
}, 'Removal detached registrations and deleted or queued quarantine')
|
||||
await assertEvacuated('Removal evacuated the player to a loaded world')
|
||||
context.report.worldLifecycle = { world: worldKey, cleanup: /cleanup=(deleted|queued)\b/.exec(finalStatus)[1] }
|
||||
})
|
||||
if (process.env.IRIS_QA_PACK_URL) {
|
||||
await context.step('download and overwrite the fixture pack through Iris', async () => {
|
||||
const response = await context.command(
|
||||
`/iris download link=${process.env.IRIS_QA_PACK_URL} overwrite=true`,
|
||||
/Iris pack 'native-terrain' installed|Iris pack download failed\./,
|
||||
120000
|
||||
)
|
||||
context.expect(response.includes("Iris pack 'native-terrain' installed"), response)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
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.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class CommandPackValidationPublicationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@After
|
||||
public void clearValidation() {
|
||||
PackValidationRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedCachedPackCannotBePersistedAsFreshValidation() throws Exception {
|
||||
File packs = temporaryFolder.newFolder("packs");
|
||||
File root = new File(packs, "terrain");
|
||||
Files.createDirectories(root.toPath());
|
||||
File content = new File(root, "dimension.json");
|
||||
Files.writeString(content.toPath(), "original");
|
||||
File cacheFile = new File(temporaryFolder.getRoot(), "validation-cache.json");
|
||||
Iris previousPlugin = Iris.instance;
|
||||
Iris plugin = mock(Iris.class);
|
||||
Iris.instance = plugin;
|
||||
when(plugin.getDataFile("cache", "pack-validation.json")).thenReturn(cacheFile);
|
||||
try (MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class, CALLS_REAL_METHODS)) {
|
||||
cache.when(PackValidationCache::contextFingerprint).thenReturn("context");
|
||||
PackValidationResult valid = new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||
PackValidationRegistry.publish(valid);
|
||||
PackValidationRegistry.publish(root.toPath(), valid,
|
||||
PackFingerprints.computePackTreeFingerprint(root), "context");
|
||||
Files.writeString(content.toPath(), "changed");
|
||||
Method persist = CommandPack.class.getDeclaredMethod("persistValidationCache", File.class);
|
||||
persist.setAccessible(true);
|
||||
|
||||
persist.invoke(new CommandPack(), packs);
|
||||
|
||||
assertFalse(cacheFile.exists());
|
||||
Files.writeString(content.toPath(), "original");
|
||||
|
||||
persist.invoke(new CommandPack(), packs);
|
||||
|
||||
assertTrue(cacheFile.exists());
|
||||
assertTrue(PackValidationCache.load(cacheFile.toPath(),
|
||||
PackFingerprints.computePackContentSnapshot(packs).content(), "context",
|
||||
List.of(root.getName())).orElseThrow().getFirst().isLoadable());
|
||||
} finally {
|
||||
Iris.instance = previousPlugin;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void commandPublishesCanonicalRootValidation() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
PackValidationResult valid = new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(valid);
|
||||
|
||||
assertSame(valid, new CommandPack().runValidate(mock(VolmitSender.class), root));
|
||||
|
||||
assertSame(valid, PackValidationRegistry.get(root.toPath()));
|
||||
assertSame(valid, PackValidationRegistry.get(root.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mutationDuringValidationCannotRepublishStaleResult() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(root.toPath())) {
|
||||
return new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||
}
|
||||
});
|
||||
|
||||
assertFalse(new CommandPack().runValidate(mock(VolmitSender.class), root).isLoadable());
|
||||
assertNull(PackValidationRegistry.get(root.toPath()));
|
||||
assertNull(PackValidationRegistry.get(root.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalEditDuringValidationPublishesBlockedResult() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
File content = new File(root, "dimension.json");
|
||||
Files.writeString(content.toPath(), "original");
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||
Files.writeString(content.toPath(), "changed");
|
||||
return new PackValidationResult(root.getName(), List.of(), List.of(), 1L);
|
||||
});
|
||||
|
||||
assertFalse(new CommandPack().runValidate(mock(VolmitSender.class), root).isLoadable());
|
||||
assertFalse(PackValidationRegistry.get(root.toPath()).isLoadable());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -50,7 +50,7 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
|
||||
int xx = rX + dx;
|
||||
int yy = y + dy;
|
||||
int zz = rZ + dz;
|
||||
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()
|
||||
return xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy >= hunk.getHeight()
|
||||
? null : hunk.get(xx, yy, zz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
|
||||
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
|
||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedTreeFellerService;
|
||||
@@ -477,6 +478,7 @@ public final class ModdedEngineBootstrap {
|
||||
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
createdServices.register(ModdedPackUpdateService.class, new ModdedPackUpdateService());
|
||||
ModdedStudioHotloadService studioHotloadService = createdServices.register(
|
||||
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Owns the Iris generation pool and the single decision of whether this loader already runs chunk
|
||||
@@ -43,7 +42,7 @@ public final class ModdedGenPool {
|
||||
private static final long SHUTDOWN_DRAIN_MILLIS = 2_000L;
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final ChunkSystem CHUNK_SYSTEM = detectChunkSystem();
|
||||
private static final AtomicReference<ExecutorService> GEN_POOL = new AtomicReference<>(createGenPool());
|
||||
private static ExecutorService generationPool = createGenPool();
|
||||
|
||||
private ModdedGenPool() {
|
||||
}
|
||||
@@ -63,39 +62,32 @@ public final class ModdedGenPool {
|
||||
return CHUNK_SYSTEM.description();
|
||||
}
|
||||
|
||||
static ExecutorService pool() {
|
||||
ExecutorService pool = GEN_POOL.get();
|
||||
if (pool != null && !pool.isShutdown()) {
|
||||
return pool;
|
||||
}
|
||||
start();
|
||||
ExecutorService restarted = GEN_POOL.get();
|
||||
if (restarted == null) {
|
||||
static synchronized ExecutorService pool() {
|
||||
if (generationPool.isShutdown()) {
|
||||
throw new RejectedExecutionException("Iris gen pool is shut down");
|
||||
}
|
||||
return restarted;
|
||||
return generationPool;
|
||||
}
|
||||
|
||||
static void start() {
|
||||
while (true) {
|
||||
ExecutorService current = GEN_POOL.get();
|
||||
if (current != null && !current.isShutdown()) {
|
||||
static synchronized void start() {
|
||||
if (!generationPool.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
ExecutorService created = createGenPool();
|
||||
if (GEN_POOL.compareAndSet(current, created)) {
|
||||
return;
|
||||
}
|
||||
created.shutdownNow();
|
||||
if (!generationPool.isTerminated()) {
|
||||
throw new IllegalStateException("Iris gen pool cannot restart while prior workers are still active");
|
||||
}
|
||||
generationPool = createGenPool();
|
||||
}
|
||||
|
||||
static void shutdown() {
|
||||
ExecutorService pool = GEN_POOL.getAndSet(null);
|
||||
if (pool == null) {
|
||||
ExecutorService pool;
|
||||
synchronized (ModdedGenPool.class) {
|
||||
pool = generationPool;
|
||||
if (pool.isTerminated()) {
|
||||
return;
|
||||
}
|
||||
pool.shutdown();
|
||||
}
|
||||
try {
|
||||
if (pool.awaitTermination(SHUTDOWN_DRAIN_MILLIS, TimeUnit.MILLISECONDS)) {
|
||||
return;
|
||||
|
||||
@@ -20,13 +20,11 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashRenderer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class ModdedIrisSplash {
|
||||
|
||||
@@ -34,18 +32,11 @@ public final class ModdedIrisSplash {
|
||||
}
|
||||
|
||||
public static void print(NativeModdedLoader loader) {
|
||||
printPacks(loader);
|
||||
if (isLogoEnabled()) {
|
||||
printLogo(loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printPacks(NativeModdedLoader loader) {
|
||||
File packFolder = IrisPlatforms.get().packsFolderNoCreate();
|
||||
IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)
|
||||
.thenAccept(lines -> lines.forEach(IrisLogging::info));
|
||||
}
|
||||
|
||||
private static void printLogo(NativeModdedLoader loader) {
|
||||
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
|
||||
String[] splash = IrisSplashRenderer.renderPlain();
|
||||
|
||||
+41
-20
@@ -27,16 +27,15 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public final class ModdedPrimaryWorldRouter {
|
||||
private static final int TICK_INTERVAL = 20;
|
||||
|
||||
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<UUID> inFlight = ConcurrentHashMap.newKeySet();
|
||||
private static final ConcurrentMap<UUID, CompletableFuture<Boolean>> routes = new ConcurrentHashMap<>();
|
||||
private static int tickCounter = 0;
|
||||
private static volatile String loginRefusal;
|
||||
private static volatile String absentPrimary;
|
||||
@@ -45,8 +44,12 @@ public final class ModdedPrimaryWorldRouter {
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
routed.clear();
|
||||
inFlight.clear();
|
||||
routes.forEach((id, route) -> {
|
||||
if (routes.remove(id, route)) {
|
||||
route.cancel(false);
|
||||
}
|
||||
});
|
||||
tickCounter = 0;
|
||||
loginRefusal = null;
|
||||
absentPrimary = null;
|
||||
}
|
||||
@@ -57,8 +60,10 @@ public final class ModdedPrimaryWorldRouter {
|
||||
*/
|
||||
public static void forget(UUID player) {
|
||||
if (player != null) {
|
||||
routed.remove(player);
|
||||
inFlight.remove(player);
|
||||
CompletableFuture<Boolean> route = routes.remove(player);
|
||||
if (route != null) {
|
||||
route.cancel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,36 +115,52 @@ public final class ModdedPrimaryWorldRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
server.forEachPlayer(player -> {
|
||||
server.forEachPlayer(player -> routePlayer(server, player, overworld, target, primary));
|
||||
}
|
||||
|
||||
private static void routePlayer(NativeModdedServer server, NativeProtocolPlayer player,
|
||||
NativeWorld overworld, NativeWorld target, String primary) {
|
||||
UUID id = player.id();
|
||||
if (routed.contains(id) || !inFlight.add(id)) {
|
||||
if (routes.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<Boolean> route = new CompletableFuture<>();
|
||||
if (routes.putIfAbsent(id, route) != null) {
|
||||
return;
|
||||
}
|
||||
route.whenComplete((success, failure) -> {
|
||||
if (!Boolean.TRUE.equals(success)) {
|
||||
routes.remove(id, route);
|
||||
}
|
||||
});
|
||||
if (!player.isInWorld(overworld)) {
|
||||
inFlight.remove(id);
|
||||
routed.add(id);
|
||||
route.complete(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<Boolean> teleport = NativeWorldTeleport.teleport(player,
|
||||
new NativeWorldTeleport.Destination(server, target, player.x(), Double.MIN_VALUE, player.z(),
|
||||
ModdedTeleportDeadline.fromNow()));
|
||||
teleport.whenComplete((success, failure) -> {
|
||||
inFlight.remove(id);
|
||||
if (Boolean.TRUE.equals(success) && failure == null) {
|
||||
routed.add(id);
|
||||
return;
|
||||
route.whenComplete((success, failure) -> {
|
||||
if (!Boolean.TRUE.equals(success) && !teleport.isDone()) {
|
||||
teleport.cancel(false);
|
||||
}
|
||||
});
|
||||
teleport.whenComplete((success, failure) -> {
|
||||
if (failure != null) {
|
||||
if (route.completeExceptionally(failure)) {
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'",
|
||||
id, primary, failure);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
inFlight.remove(id);
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
||||
} else {
|
||||
route.complete(Boolean.TRUE.equals(success));
|
||||
}
|
||||
});
|
||||
} catch (Throwable failure) {
|
||||
if (route.completeExceptionally(failure)) {
|
||||
ModdedIrisLog.error("Iris failed to route player {} to primary world '{}'", id, primary, failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.platform.protocol.IrisVisionRequestService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -128,6 +129,10 @@ public final class ModdedProtocolHandler {
|
||||
}
|
||||
String sessionId = sessionId(player);
|
||||
ModdedStartup.warnStartupFailuresTo(player);
|
||||
ModdedPackUpdateService packUpdates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||
if (packUpdates != null) {
|
||||
packUpdates.notifyPlayer(player);
|
||||
}
|
||||
IrisSessionRegistry current = registry;
|
||||
ModdedProtocolTransport currentTransport = transport;
|
||||
if (current == null || currentTransport == null) {
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.pack.PackFingerprints;
|
||||
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackDownloader;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||
@@ -31,15 +34,11 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedStartup {
|
||||
private static final int COMPAT_BOOT_KEY_CAP = 3;
|
||||
@@ -60,36 +59,9 @@ public final class ModdedStartup {
|
||||
if (!PREPARED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reportLegacyPacksDirectory();
|
||||
validateAllPacks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Older builds mkdir'd (and stale guidance sometimes populated) config/iris/packs, but modded
|
||||
* packs live under config/irisworldgen/packs. Never auto-move user content: warn loudly when
|
||||
* the legacy directory holds packs, and quietly remove it when it is empty.
|
||||
*/
|
||||
private static void reportLegacyPacksDirectory() {
|
||||
try {
|
||||
File legacy = ModdedEngineBootstrap.loader().configDir().resolve("iris").resolve("packs").toFile();
|
||||
if (!legacy.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
if (!PackDirectoryResolver.listVisiblePackDirectories(legacy).isEmpty()) {
|
||||
File real = art.arcane.iris.spi.IrisPlatforms.get().packsFolderNoCreate();
|
||||
ModdedIrisLog.warn("Iris found packs under the legacy directory {} - modded packs load from {} only. Move them there.",
|
||||
legacy.getAbsolutePath(), real.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
String[] entries = legacy.list();
|
||||
if (entries == null || entries.length == 0) {
|
||||
legacy.delete();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.debug("Iris legacy packs directory check failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot trigger for the forced datapack. Runs on its own daemon thread rather than the Iris scheduler:
|
||||
* ModdedEngineBootstrap.start clears the async queue at SERVER_STARTING, which would silently drop this
|
||||
@@ -140,8 +112,7 @@ public final class ModdedStartup {
|
||||
}
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
PackValidationResult result = validatePack(packDir);
|
||||
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
||||
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
||||
String compatSuffix = compatSummary.isEmpty() ? "" : " " + compatSummary;
|
||||
@@ -184,16 +155,15 @@ public final class ModdedStartup {
|
||||
throw new BrokenPackException(pack, List.of(
|
||||
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
|
||||
}
|
||||
PackValidationResult cached = PackValidationRegistry.get(pack);
|
||||
if (cached != null && cached.getValidatedAtMillis() >= newestModificationMillis(packDir.toPath())) {
|
||||
// prepareForStartup already validated this pack and nothing in it changed since; re-validating per
|
||||
// persistent dimension at boot costs a full pack parse each time.
|
||||
return PackValidationRegistry.requireLoadable(pack);
|
||||
}
|
||||
Path packRoot = packDir.toPath();
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
return PackValidationRegistry.requireLoadable(pack);
|
||||
PackValidationResult cached = PackValidationRegistry.getMatching(
|
||||
packRoot, PackFingerprints.computePackTreeFingerprint(packDir));
|
||||
if (cached != null) {
|
||||
return PackValidationRegistry.requireLoadable(packRoot);
|
||||
}
|
||||
validatePack(packDir);
|
||||
return PackValidationRegistry.requireLoadable(packRoot);
|
||||
} catch (BrokenPackException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
@@ -212,6 +182,42 @@ public final class ModdedStartup {
|
||||
}
|
||||
}
|
||||
|
||||
public static PackValidationResult validatePack(File packDir) {
|
||||
Path packRoot = packDir.toPath();
|
||||
PackValidationRegistry.ValidationTicket ticket = PackValidationRegistry.tryBeginValidation(packRoot);
|
||||
if (ticket == null) {
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content is currently being changed."));
|
||||
}
|
||||
String contentFingerprint = PackFingerprints.computePackTreeFingerprint(packDir);
|
||||
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||
PackValidationResult result;
|
||||
try {
|
||||
result = PackValidator.validate(packDir);
|
||||
} catch (Throwable failure) {
|
||||
ModdedIrisLog.error("Iris pack validation failed for '{}'", packDir.getName(), failure);
|
||||
String detail = failure.getMessage();
|
||||
if (detail == null || detail.isBlank()) {
|
||||
detail = failure.getClass().getSimpleName();
|
||||
}
|
||||
result = new PackValidationResult(
|
||||
packDir.getName(),
|
||||
List.of("Pack validation failed with " + failure.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packDir))
|
||||
|| !contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||
PackValidationRegistry.remove(packRoot);
|
||||
PackValidationRegistry.remove(packDir.getName());
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content or validation context changed during validation."));
|
||||
}
|
||||
if (!PackValidationRegistry.publishIfCurrent(ticket, result, contentFingerprint, contextFingerprint)) {
|
||||
throw new BrokenPackException(packDir.getName(), List.of("Pack content changed during validation."));
|
||||
}
|
||||
PackValidationRegistry.publish(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void reinjectPersistentDimensions(NativeModdedServer server) {
|
||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions =
|
||||
ModdedDimensionRegistryStore.loadForStartup(server);
|
||||
@@ -266,23 +272,6 @@ public final class ModdedStartup {
|
||||
+ GenerationRefusalNotice.summary(failure));
|
||||
}
|
||||
|
||||
private static long newestModificationMillis(Path root) {
|
||||
long newest = 0L;
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
for (Path path : (Iterable<Path>) walk::iterator) {
|
||||
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
long modified = attributes.lastModifiedTime().toMillis();
|
||||
if (modified > newest) {
|
||||
newest = modified;
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException unreadable) {
|
||||
ModdedIrisLog.debug("Iris could not stat {} for validation reuse; revalidating", root, unreadable);
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
/**
|
||||
* True once the first tick with a player list has re-injected the persistent dimensions. Before that a runtime
|
||||
* dimension that is not loaded yet is still on its way.
|
||||
@@ -296,7 +285,7 @@ public final class ModdedStartup {
|
||||
* the console. Tell the operators who can actually act on it when they join.
|
||||
*/
|
||||
public static void warnStartupFailuresTo(NativeProtocolPlayer player) {
|
||||
if (player == null || !player.isGameMaster()) {
|
||||
if (player == null || (!player.isGameMaster() && !player.isServerOwner())) {
|
||||
return;
|
||||
}
|
||||
for (String failure : DIMENSION_FAILURES) {
|
||||
|
||||
+7
@@ -33,6 +33,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.service.ModdedPackUpdateService;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
@@ -445,6 +446,12 @@ public final class IrisModdedCommands {
|
||||
);
|
||||
String completionMessage = downloadCompletionMessage(result);
|
||||
if (result != null) {
|
||||
if (result.changed()) {
|
||||
ModdedPackUpdateService updates = ModdedEngineBootstrap.services().service(ModdedPackUpdateService.class);
|
||||
if (updates != null) {
|
||||
dispatchDownloadFeedback(source, updates::refresh);
|
||||
}
|
||||
}
|
||||
if (completionMessage != null) {
|
||||
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
|
||||
}
|
||||
|
||||
+3
-3
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.modded.ModdedStartup;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -25,7 +27,6 @@ import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
@@ -138,8 +139,7 @@ public final class ModdedPackCommands {
|
||||
int broken = 0;
|
||||
for (File target : targets) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(target);
|
||||
PackValidationRegistry.publish(result);
|
||||
PackValidationResult result = ModdedStartup.validatePack(target);
|
||||
if (!result.isLoadable()) {
|
||||
broken++;
|
||||
}
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
|
||||
import art.arcane.iris.pack.BuiltInPackUpdates;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class ModdedPackUpdateService implements ModdedTickableService {
|
||||
private final Function<File, CompletableFuture<PackCheck>> checker;
|
||||
private final Set<UUID> notified = new HashSet<>();
|
||||
private CompletableFuture<PackCheck> pending;
|
||||
private List<String> notices = List.of();
|
||||
private File packsFolder;
|
||||
private boolean enabled;
|
||||
private boolean started;
|
||||
|
||||
public ModdedPackUpdateService() {
|
||||
this(ModdedPackUpdateService::checkPacks);
|
||||
}
|
||||
|
||||
ModdedPackUpdateService(Function<File, CompletableFuture<PackCheck>> checker) {
|
||||
this.checker = checker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
enabled = false;
|
||||
if (pending != null) {
|
||||
pending.cancel(true);
|
||||
pending = null;
|
||||
}
|
||||
started = false;
|
||||
notices = List.of();
|
||||
notified.clear();
|
||||
packsFolder = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(NativeModdedServer server) {
|
||||
if (!enabled || server == null || !server.hasPlayerList()) {
|
||||
return;
|
||||
}
|
||||
if (!started) {
|
||||
started = true;
|
||||
packsFolder = IrisPlatforms.get().packsFolderNoCreate().getAbsoluteFile();
|
||||
pending = checker.apply(packsFolder);
|
||||
}
|
||||
if (pending == null || !pending.isDone()) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<PackCheck> completed = pending;
|
||||
pending = null;
|
||||
try {
|
||||
publish(completed.join());
|
||||
server.forEachPlayer(this::notifyPlayer);
|
||||
} catch (CompletionException error) {
|
||||
IrisLogging.reportError("Iris pack update check failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (pending != null) {
|
||||
pending.cancel(true);
|
||||
pending = null;
|
||||
}
|
||||
started = false;
|
||||
notices = List.of();
|
||||
notified.clear();
|
||||
}
|
||||
|
||||
public void notifyPlayer(NativeProtocolPlayer player) {
|
||||
if (!enabled || notices.isEmpty() || player == null || !player.connected()
|
||||
|| (!player.isGameMaster() && !player.isServerOwner()) || !notified.add(player.id())) {
|
||||
return;
|
||||
}
|
||||
for (String notice : notices) {
|
||||
player.sendMessage(notice);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<PackCheck> checkPacks(File folder) {
|
||||
return CompletableFuture.supplyAsync(() -> IrisSplashPackScanner.collect(folder, IrisLogging::reportError))
|
||||
.thenCompose(packs -> BuiltInPackUpdates.check(packs.stream().map(SplashPackMetadata::name).toList())
|
||||
.thenApply(updates -> new PackCheck(packs, updates)));
|
||||
}
|
||||
|
||||
private void publish(PackCheck result) {
|
||||
List<String> updates = new ArrayList<>();
|
||||
IrisLogging.info("Iris packs: " + packsFolder);
|
||||
for (SplashPackMetadata pack : result.packs()) {
|
||||
BuiltInPackUpdates.Update update = result.updates().get(pack.name());
|
||||
String version = pack.version().matches("[0-9]+") ? "v" + pack.version() : "version unknown";
|
||||
IrisLogging.info(" " + pack.name() + " " + version
|
||||
+ (update == null ? "" : update.suffix(pack.version())));
|
||||
if (update != null && update.newerThan(pack.version())) {
|
||||
updates.add("Iris " + pack.name() + " release metadata is outdated: installed v" + pack.version()
|
||||
+ ", latest built-in release v" + update.version() + ".");
|
||||
updates.add("Back up and review custom edits in " + new File(packsFolder, pack.name())
|
||||
+ "; close Studio, then update with /iris download pack=" + pack.name() + " overwrite=true.");
|
||||
updates.add("Existing worlds retain their recorded production pack; this updates the authoring pack for new worlds.");
|
||||
}
|
||||
}
|
||||
notices = List.copyOf(updates);
|
||||
for (String notice : notices) {
|
||||
IrisLogging.warn(notice);
|
||||
}
|
||||
}
|
||||
|
||||
record PackCheck(List<SplashPackMetadata> packs, Map<String, BuiltInPackUpdates.Update> updates) {
|
||||
PackCheck {
|
||||
packs = List.copyOf(packs);
|
||||
updates = Map.copyOf(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-5
@@ -57,6 +57,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStudioHotloadService implements ModdedTickableService, EnginePlatformHooks {
|
||||
@@ -64,6 +65,8 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private static final long POLL_MILLIS = 250L;
|
||||
private static final long CHECK_LATCH_MILLIS = 1_000L;
|
||||
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
|
||||
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
|
||||
private static final long INTERRUPT_DRAIN_TIMEOUT_SECONDS = 5L;
|
||||
|
||||
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
|
||||
private volatile ExecutorService executor;
|
||||
@@ -81,9 +84,13 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (executor != null) {
|
||||
ExecutorService current = executor;
|
||||
if (current != null && !current.isTerminated()) {
|
||||
if (!current.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Iris Studio hotload cannot restart while prior workers are still active");
|
||||
}
|
||||
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
|
||||
Thread thread = new Thread(task, "Iris Studio Hotload");
|
||||
thread.setDaemon(true);
|
||||
@@ -96,10 +103,10 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = executor;
|
||||
executor = null;
|
||||
if (active != null) {
|
||||
active.shutdownNow();
|
||||
if (!shutdownAndDrain(active)) {
|
||||
throw new IllegalStateException("Iris Studio hotload worker remained active during service shutdown");
|
||||
}
|
||||
executor = null;
|
||||
for (Watch watch : watches.values()) {
|
||||
watch.close();
|
||||
}
|
||||
@@ -197,7 +204,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
@Override
|
||||
public void onServerTick(NativeModdedServer server) {
|
||||
ExecutorService active = executor;
|
||||
if (active == null) {
|
||||
if (active == null || active.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -284,6 +291,30 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
&& !isPregeneratorActive(engine);
|
||||
}
|
||||
|
||||
private static boolean shutdownAndDrain(ExecutorService active) {
|
||||
if (active == null) {
|
||||
return true;
|
||||
}
|
||||
active.shutdown();
|
||||
try {
|
||||
if (active.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
active.shutdownNow();
|
||||
if (active.awaitTermination(INTERRUPT_DRAIN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
return true;
|
||||
}
|
||||
ModdedIrisLog.error("Iris Studio hotload worker did not stop after shutdownNow",
|
||||
new IllegalStateException("Iris Studio hotload worker is still active"));
|
||||
return false;
|
||||
} catch (InterruptedException exception) {
|
||||
active.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
ModdedIrisLog.error("Interrupted while draining Iris Studio hotload", exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
||||
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
||||
return true;
|
||||
|
||||
+31
@@ -39,6 +39,37 @@ public class ModdedClientPackageIsolationTest {
|
||||
"art/arcane/iris/nativegen");
|
||||
private static final int MINIMUM_SCANNED = 50;
|
||||
|
||||
@Test
|
||||
public void clientPresentationCannotAccessPackOrWorldPersistence() throws IOException, URISyntaxException {
|
||||
Path root = classesRoot();
|
||||
Path directory = root.resolve(CLIENT_IRIS);
|
||||
List<String> forbidden = List.of(
|
||||
"art/arcane/iris/pack/",
|
||||
"art/arcane/iris/world/storage/",
|
||||
"art/arcane/iris/world/history/",
|
||||
"art/arcane/iris/generation/runtime/",
|
||||
"art/arcane/iris/configuration/",
|
||||
"java/nio/file/",
|
||||
"java/nio/channels/FileChannel",
|
||||
"java/io/File",
|
||||
"java/io/RandomAccessFile");
|
||||
List<String> violations = new ArrayList<>();
|
||||
int scanned = 0;
|
||||
try (Stream<Path> walk = Files.walk(directory)) {
|
||||
for (Path classFile : walk.filter(ModdedClientPackageIsolationTest::isClassFile).toList()) {
|
||||
scanned++;
|
||||
String bytecode = readAsLatin1(classFile);
|
||||
for (String dependency : forbidden) {
|
||||
if (bytecode.contains(dependency)) {
|
||||
violations.add(root.relativize(classFile) + " -> " + dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("client presentation classes were not scanned", scanned >= 20);
|
||||
assertEquals("client presentation can access persistent generation data: " + violations, List.of(), violations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverSidePackagesNeverReferenceClientOnlyTypes() throws IOException, URISyntaxException {
|
||||
Path root = classesRoot();
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockFaces;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedDecoratorHooksTest {
|
||||
@Test
|
||||
public void faceFixingExcludesTheUpperBoundaryWhileKeepingTheLastRowAccessible() {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
NativeBlockState lastRow = mock(NativeBlockState.class);
|
||||
Hunk<NativeBlockState> hunk = mock(Hunk.class);
|
||||
when(hunk.getHeight()).thenReturn(4);
|
||||
when(hunk.get(1, 3, 2)).thenReturn(lastRow);
|
||||
when(hunk.get(1, 4, 2)).thenThrow(new IndexOutOfBoundsException("upper boundary"));
|
||||
try (MockedStatic<NativeBlockFaces> faces = mockStatic(NativeBlockFaces.class)) {
|
||||
faces.when(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
NativeBlockFaces.Neighbors neighbors = invocation.getArgument(1);
|
||||
assertNull(neighbors.secondary(0, 1, 0));
|
||||
assertSame(lastRow, neighbors.secondary(0, 0, 0));
|
||||
return state;
|
||||
});
|
||||
|
||||
assertSame(state, new ModdedDecoratorHooks().fixFaces(state, hunk, 1, 2, 99, 3, 108, null));
|
||||
|
||||
faces.verify(() -> NativeBlockFaces.fixFaces(eq(state), any(NativeBlockFaces.Neighbors.class)));
|
||||
}
|
||||
verify(hunk, never()).get(1, 4, 2);
|
||||
verify(hunk).get(1, 3, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ModdedGenPoolTest {
|
||||
@Test
|
||||
public void executorLookupDoesNotRestartStoppedPool() throws Exception {
|
||||
ModdedGenPool.start();
|
||||
ExecutorService original = ModdedGenPool.pool();
|
||||
ModdedGenPool.shutdown();
|
||||
try {
|
||||
ModdedGenPool.pool();
|
||||
fail("Stopped generation pool must reject executor lookup");
|
||||
} catch (RejectedExecutionException expected) {
|
||||
assertTrue(original.isShutdown());
|
||||
}
|
||||
ModdedGenPool.start();
|
||||
try {
|
||||
assertNotSame(original, ModdedGenPool.pool());
|
||||
} finally {
|
||||
ModdedGenPool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restartWaitsForPreviousWorkersToTerminate() throws Exception {
|
||||
ModdedGenPool.start();
|
||||
ExecutorService original = ModdedGenPool.pool();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
original.execute(() -> {
|
||||
entered.countDown();
|
||||
boolean released = false;
|
||||
while (!released) {
|
||||
try {
|
||||
release.await();
|
||||
released = true;
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
});
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
try {
|
||||
Thread.currentThread().interrupt();
|
||||
ModdedGenPool.shutdown();
|
||||
assertTrue(Thread.interrupted());
|
||||
assertFalse(original.isTerminated());
|
||||
try {
|
||||
ModdedGenPool.start();
|
||||
fail("Generation pool must retain workers until they terminate");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertFalse(original.isTerminated());
|
||||
}
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
release.countDown();
|
||||
assertTrue(original.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
ModdedGenPool.start();
|
||||
assertNotSame(original, ModdedGenPool.pool());
|
||||
ExecutorService restarted = ModdedGenPool.pool();
|
||||
ModdedGenPool.start();
|
||||
assertSame(restarted, ModdedGenPool.pool());
|
||||
ModdedGenPool.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.modded.command.ModdedPackCommands;
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import org.junit.After;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class ModdedPackValidationPublicationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@After
|
||||
public void clearValidation() {
|
||||
PackValidationRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validationPublishesTheCanonicalRootAndPackName() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
File equivalent = new File(root, ".");
|
||||
PackValidationResult result = loadable(root);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(equivalent)).thenReturn(result);
|
||||
|
||||
assertSame(result, ModdedStartup.validatePack(equivalent));
|
||||
|
||||
assertSame(result, PackValidationRegistry.requireLoadable(root.toPath()));
|
||||
assertSame(result, PackValidationRegistry.requireLoadable(root.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootInvalidationCannotReusePackNameValidation() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
PackValidationRegistry.publish(loadable(root));
|
||||
PackValidationResult replacement = loadable(root);
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<ModdedPackCommands> commands = mockStatic(ModdedPackCommands.class)) {
|
||||
commands.when(ModdedPackCommands::packsRoot).thenReturn(root.getParentFile());
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(replacement);
|
||||
|
||||
assertSame(replacement, ModdedStartup.requirePackForWorldCreation(root.getName()));
|
||||
|
||||
validator.verify(() -> PackValidator.validate(root));
|
||||
assertSame(replacement, PackValidationRegistry.requireLoadable(root.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedContentWithPreservedTimestampCannotReuseValidation() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
File content = new File(root, "dimension.json");
|
||||
Files.writeString(content.toPath(), "valid");
|
||||
PackValidationResult original = loadable(root);
|
||||
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(),
|
||||
System.currentTimeMillis());
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<ModdedPackCommands> commands = mockStatic(ModdedPackCommands.class)) {
|
||||
commands.when(ModdedPackCommands::packsRoot).thenReturn(root.getParentFile());
|
||||
validator.when(() -> PackValidator.validate(root)).thenReturn(original, broken);
|
||||
ModdedStartup.validatePack(root);
|
||||
Files.writeString(content.toPath(), "wrong");
|
||||
Files.setLastModifiedTime(content.toPath(), FileTime.fromMillis(1L));
|
||||
Files.setLastModifiedTime(root.toPath(), FileTime.fromMillis(1L));
|
||||
|
||||
assertThrows(BrokenPackException.class,
|
||||
() -> ModdedStartup.requirePackForWorldCreation(root.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void externalEditDuringValidationCannotPublishMixedSnapshot() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
File content = new File(root, "dimension.json");
|
||||
Files.writeString(content.toPath(), "original");
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||
Files.writeString(content.toPath(), "changed");
|
||||
return loadable(root);
|
||||
});
|
||||
|
||||
assertThrows(BrokenPackException.class, () -> ModdedStartup.validatePack(root));
|
||||
assertNull(PackValidationRegistry.get(root.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mutationDuringValidationPreventsStalePublication() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenAnswer(invocation -> {
|
||||
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(root.toPath())) {
|
||||
return loadable(root);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
ModdedStartup.validatePack(root);
|
||||
fail("Changed pack must reject stale validation");
|
||||
} catch (BrokenPackException expected) {
|
||||
assertNull(PackValidationRegistry.get(root.toPath()));
|
||||
assertNull(PackValidationRegistry.get(root.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validationFailurePublishesBlockedRoot() throws Exception {
|
||||
File root = temporaryFolder.newFolder("terrain");
|
||||
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
|
||||
MockedStatic<ModdedIrisLog> log = mockStatic(ModdedIrisLog.class)) {
|
||||
validator.when(() -> PackValidator.validate(root)).thenThrow(new IllegalStateException("Invalid terrain"));
|
||||
|
||||
PackValidationResult result = ModdedStartup.validatePack(root);
|
||||
|
||||
assertFalse(result.isLoadable());
|
||||
assertSame(result, PackValidationRegistry.get(root.toPath()));
|
||||
assertSame(result, PackValidationRegistry.get(root.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private static PackValidationResult loadable(File root) {
|
||||
return new PackValidationResult(root.getName(), List.of(), List.of(), System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
+44
@@ -4,7 +4,10 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldTeleport;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.After;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
@@ -38,6 +41,12 @@ public class ModdedPrimaryWorldRouterTest {
|
||||
private static final int TICK_INTERVAL = 20;
|
||||
private static final String PRIMARY = "iris:primary";
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@After
|
||||
public void clearRouter() {
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
@@ -233,6 +242,41 @@ public class ModdedPrimaryWorldRouterTest {
|
||||
return new ModdedDimensionRegistryStore.PersistentDimension(PRIMARY, "overworld", "overworld", 1337L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disconnectCancelsOldRoutingWithoutAffectingReconnectedPlayer() {
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
NativeWorld overworld = mock(NativeWorld.class);
|
||||
NativeWorld primary = mock(NativeWorld.class);
|
||||
when(overworld.nativeHandle()).thenReturn(new Object());
|
||||
when(primary.nativeHandle()).thenReturn(new Object());
|
||||
when(server.overworld()).thenReturn(overworld);
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
UUID id = UUID.randomUUID();
|
||||
when(player.id()).thenReturn(id);
|
||||
when(player.isInWorld(overworld)).thenReturn(true);
|
||||
onlinePlayers(server, player);
|
||||
CompletableFuture<Boolean> oldRoute = new CompletableFuture<>();
|
||||
CompletableFuture<Boolean> newRoute = new CompletableFuture<>();
|
||||
ModdedModConfig config = config(true, PRIMARY);
|
||||
try (MockedStatic<ModdedModConfig> configs = mockStatic(ModdedModConfig.class);
|
||||
MockedStatic<ModdedDimensionManager> manager = mockStatic(ModdedDimensionManager.class);
|
||||
MockedStatic<NativeWorldTeleport> teleport = mockStatic(NativeWorldTeleport.class)) {
|
||||
configs.when(ModdedModConfig::get).thenReturn(config);
|
||||
manager.when(() -> ModdedDimensionManager.level(server, PRIMARY)).thenReturn(primary);
|
||||
teleport.when(() -> NativeWorldTeleport.teleport(any(), any())).thenReturn(oldRoute, newRoute);
|
||||
evaluate(server);
|
||||
ModdedPrimaryWorldRouter.forget(id);
|
||||
assertTrue(oldRoute.isCancelled());
|
||||
evaluate(server);
|
||||
oldRoute.complete(true);
|
||||
evaluate(server);
|
||||
teleport.verify(() -> NativeWorldTeleport.teleport(any(), any()), times(2));
|
||||
assertFalse(newRoute.isDone());
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
assertTrue(newRoute.isCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
private static void evaluate(NativeModdedServer server) {
|
||||
for (int tick = 0; tick < TICK_INTERVAL; tick++) {
|
||||
ModdedPrimaryWorldRouter.tick(server);
|
||||
|
||||
+5
@@ -57,6 +57,11 @@ public class ModdedStartupReinjectFailureTest {
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
ModdedStartup.warnStartupFailuresTo(operator);
|
||||
ModdedStartup.warnStartupFailuresTo(player);
|
||||
NativeProtocolPlayer owner = mock(NativeProtocolPlayer.class);
|
||||
when(owner.isServerOwner()).thenReturn(true);
|
||||
ModdedStartup.warnStartupFailuresTo(owner);
|
||||
verify(owner).sendMessage(argThat((String message) -> message.contains("iris:moon")
|
||||
&& message.contains("generation history is unusable")));
|
||||
|
||||
verify(operator).sendMessage(argThat((String message) -> message.contains("iris:moon")
|
||||
&& message.contains("generation history is unusable")));
|
||||
|
||||
+3
@@ -21,6 +21,8 @@ import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
@@ -98,6 +100,7 @@ public class IrisModdedCommandsTeleportTest {
|
||||
when(server.getLevel(dimension)).thenReturn(serverLevel);
|
||||
ServerPlayer serverPlayer = mock(ServerPlayer.class);
|
||||
when(serverPlayer.getUUID()).thenReturn(UUID.randomUUID());
|
||||
when(serverPlayer.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 1.8F));
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.diagnostics.splash.IrisSplashPackScanner.SplashPackMetadata;
|
||||
import art.arcane.iris.pack.BuiltInPackUpdates;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.permissions.LevelBasedPermissionSet;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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.anyString;
|
||||
import static org.mockito.ArgumentMatchers.contains;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedPackUpdateServiceTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingCheckNotifiesOnlineAndLaterOperatorsOnceOnServerTick() {
|
||||
CompletableFuture<ModdedPackUpdateService.PackCheck> pending = new CompletableFuture<>();
|
||||
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> pending);
|
||||
NativeProtocolPlayer online = player(true, false);
|
||||
NativeModdedServer server = server(online);
|
||||
File root = new File("config/irisworldgen/packs").getAbsoluteFile();
|
||||
try (MockedStatic<IrisPlatforms> platforms = platform(root)) {
|
||||
service.onEnable();
|
||||
service.onServerTick(server);
|
||||
service.notifyPlayer(online);
|
||||
verify(online, never()).sendMessage(anyString());
|
||||
pending.complete(outdated());
|
||||
verify(online, never()).sendMessage(anyString());
|
||||
service.onServerTick(server);
|
||||
service.onServerTick(server);
|
||||
service.notifyPlayer(online);
|
||||
verify(online, times(1)).sendMessage(contains("installed v1, latest built-in release v2"));
|
||||
verify(online, times(1)).sendMessage(contains(new File(root, "overworld").toString()));
|
||||
verify(online, times(1)).sendMessage(contains("/iris download pack=overworld overwrite=true"));
|
||||
verify(online, never()).sendMessage(contains("plugins/"));
|
||||
NativeProtocolPlayer later = player(true, false);
|
||||
service.notifyPlayer(later);
|
||||
verify(later).sendMessage(contains("release metadata is outdated"));
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integratedOwnerWithoutCheatsReceivesNoticeAndRemoteOrdinaryPlayersDoNot() {
|
||||
NativeProtocolPlayer owner = player(false, true);
|
||||
NativeProtocolPlayer remote = player(false, false);
|
||||
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> CompletableFuture.completedFuture(outdated()));
|
||||
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
|
||||
service.onEnable();
|
||||
service.onServerTick(server(owner));
|
||||
verify(owner).sendMessage(contains("release metadata is outdated"));
|
||||
service.notifyPlayer(remote);
|
||||
verify(remote, never()).sendMessage(anyString());
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disableCancelsCheckAndRestartDoesNotPublishOldNotices() {
|
||||
CompletableFuture<ModdedPackUpdateService.PackCheck> first = new CompletableFuture<>();
|
||||
CompletableFuture<ModdedPackUpdateService.PackCheck> second = new CompletableFuture<>();
|
||||
AtomicInteger checks = new AtomicInteger();
|
||||
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> checks.getAndIncrement() == 0 ? first : second);
|
||||
NativeProtocolPlayer operator = player(true, false);
|
||||
NativeModdedServer server = server(operator);
|
||||
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
|
||||
service.onEnable();
|
||||
service.onServerTick(server);
|
||||
service.onDisable();
|
||||
assertTrue(first.isCancelled());
|
||||
assertFalse(first.complete(outdated()));
|
||||
service.onEnable();
|
||||
service.onServerTick(server);
|
||||
second.complete(new ModdedPackUpdateService.PackCheck(List.of(), Map.of()));
|
||||
service.onServerTick(server);
|
||||
verify(operator, never()).sendMessage(anyString());
|
||||
assertEquals(2, checks.get());
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentCustomUnknownAndUnavailableVersionsNeverBecomeOutdated() {
|
||||
ModdedPackUpdateService.PackCheck result = new ModdedPackUpdateService.PackCheck(List.of(
|
||||
new SplashPackMetadata("overworld", "unknown"),
|
||||
new SplashPackMetadata("underworld", "3"),
|
||||
new SplashPackMetadata("custom", "1")), Map.of(
|
||||
"overworld", new BuiltInPackUpdates.Update("2"),
|
||||
"underworld", new BuiltInPackUpdates.Update("2")));
|
||||
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> CompletableFuture.completedFuture(result));
|
||||
NativeProtocolPlayer operator = player(true, false);
|
||||
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
|
||||
service.onEnable();
|
||||
service.onServerTick(server(operator));
|
||||
verify(operator, never()).sendMessage(anyString());
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulAuthoringUpdateClearsOldNoticesAndRescansInstalledVersion() {
|
||||
CompletableFuture<ModdedPackUpdateService.PackCheck> current = new CompletableFuture<>();
|
||||
AtomicInteger checks = new AtomicInteger();
|
||||
ModdedPackUpdateService service = new ModdedPackUpdateService(folder -> checks.getAndIncrement() == 0
|
||||
? CompletableFuture.completedFuture(outdated()) : current);
|
||||
NativeProtocolPlayer operator = player(true, false);
|
||||
NativeModdedServer server = server(operator);
|
||||
try (MockedStatic<IrisPlatforms> platforms = platform(new File("config/irisworldgen/packs"))) {
|
||||
service.onEnable();
|
||||
service.onServerTick(server);
|
||||
verify(operator).sendMessage(contains("release metadata is outdated"));
|
||||
service.refresh();
|
||||
NativeProtocolPlayer later = player(true, false);
|
||||
service.notifyPlayer(later);
|
||||
verify(later, never()).sendMessage(anyString());
|
||||
service.onServerTick(server);
|
||||
current.complete(new ModdedPackUpdateService.PackCheck(List.of(new SplashPackMetadata("overworld", "2")),
|
||||
Map.of("overworld", new BuiltInPackUpdates.Update("2"))));
|
||||
service.onServerTick(server);
|
||||
service.notifyPlayer(later);
|
||||
verify(later, never()).sendMessage(anyString());
|
||||
assertEquals(2, checks.get());
|
||||
service.onDisable();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeOwnerCheckDoesNotGrantGameMasterPermissions() {
|
||||
ServerPlayer handle = mock(ServerPlayer.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
when(handle.level()).thenReturn(level);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(handle.permissions()).thenReturn(LevelBasedPermissionSet.ALL);
|
||||
NativeProtocolPlayer player = NativeProtocolPlayer.fromHandle(handle);
|
||||
when(server.isSingleplayerOwner(handle.nameAndId())).thenReturn(true);
|
||||
assertTrue(player.isServerOwner());
|
||||
assertFalse(player.isGameMaster());
|
||||
when(server.isSingleplayerOwner(handle.nameAndId())).thenReturn(false);
|
||||
assertFalse(player.isServerOwner());
|
||||
assertFalse(player.isGameMaster());
|
||||
}
|
||||
|
||||
private static ModdedPackUpdateService.PackCheck outdated() {
|
||||
return new ModdedPackUpdateService.PackCheck(List.of(new SplashPackMetadata("overworld", "1")),
|
||||
Map.of("overworld", new BuiltInPackUpdates.Update("2")));
|
||||
}
|
||||
|
||||
private static MockedStatic<IrisPlatforms> platform(File root) {
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.packsFolderNoCreate()).thenReturn(root);
|
||||
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||
return platforms;
|
||||
}
|
||||
|
||||
private static NativeProtocolPlayer player(boolean operator, boolean owner) {
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
when(player.id()).thenReturn(UUID.randomUUID());
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.isGameMaster()).thenReturn(operator);
|
||||
when(player.isServerOwner()).thenReturn(owner);
|
||||
return player;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static NativeModdedServer server(NativeProtocolPlayer player) {
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
when(server.hasPlayerList()).thenReturn(true);
|
||||
doAnswer(invocation -> {
|
||||
invocation.getArgument(0, Consumer.class).accept(player);
|
||||
return null;
|
||||
}).when(server).forEachPlayer(any());
|
||||
return server;
|
||||
}
|
||||
}
|
||||
+57
@@ -4,14 +4,65 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.modded.ModdedScheduler;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedStudioHotloadServiceTest {
|
||||
@Test
|
||||
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
ExecutorService executor = mock(ExecutorService.class);
|
||||
setExecutor(service, executor);
|
||||
when(executor.awaitTermination(30L, TimeUnit.SECONDS)).thenReturn(false);
|
||||
when(executor.awaitTermination(5L, TimeUnit.SECONDS)).thenReturn(true);
|
||||
|
||||
service.onDisable();
|
||||
|
||||
InOrder order = inOrder(executor);
|
||||
order.verify(executor).shutdown();
|
||||
order.verify(executor).awaitTermination(30L, TimeUnit.SECONDS);
|
||||
order.verify(executor).shutdownNow();
|
||||
order.verify(executor).awaitTermination(5L, TimeUnit.SECONDS);
|
||||
service.onEnable();
|
||||
service.onDisable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undrainedWorkerPreventsRestart() throws Exception {
|
||||
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
|
||||
ExecutorService executor = mock(ExecutorService.class);
|
||||
setExecutor(service, executor);
|
||||
when(executor.isShutdown()).thenReturn(true);
|
||||
try {
|
||||
service.onDisable();
|
||||
fail("Hotload shutdown must report a worker that did not terminate");
|
||||
} catch (IllegalStateException expected) {
|
||||
verify(executor).shutdownNow();
|
||||
}
|
||||
try {
|
||||
service.onEnable();
|
||||
fail("Hotload must not start another worker before the previous worker terminates");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertFalse(executor.isTerminated());
|
||||
}
|
||||
when(executor.isTerminated()).thenReturn(true);
|
||||
service.onEnable();
|
||||
service.onDisable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsDatapackImportsAcrossLoadedDimensions() {
|
||||
IrisDimension empty = new IrisDimension();
|
||||
@@ -45,4 +96,10 @@ public class ModdedStudioHotloadServiceTest {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
private static void setExecutor(ModdedStudioHotloadService service, ExecutorService executor) throws Exception {
|
||||
Field field = ModdedStudioHotloadService.class.getDeclaredField("executor");
|
||||
field.setAccessible(true);
|
||||
field.set(service, executor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package art.arcane.volmlib.nativelib.minecraft26_2.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
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.anyInt;
|
||||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeTeleportSafetyTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsTallAndHazardousSupportsAndAcceptsSolidFloor() {
|
||||
ServerPlayer player = player();
|
||||
ServerLevel level = level();
|
||||
for (BlockState support : new BlockState[]{Blocks.OAK_FENCE.defaultBlockState(),
|
||||
Blocks.MAGMA_BLOCK.defaultBlockState(), Blocks.CACTUS.defaultBlockState()}) {
|
||||
setSupport(level, support);
|
||||
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
|
||||
}
|
||||
setSupport(level, Blocks.STONE.defaultBlockState());
|
||||
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalPositionUsesWholePlayerBodyAndRequiresNeighborChunk() {
|
||||
ServerPlayer player = player();
|
||||
ServerLevel level = level();
|
||||
setSupport(level, Blocks.STONE.defaultBlockState());
|
||||
when(level.noCollision(any(), any(AABB.class))).thenAnswer(invocation -> {
|
||||
AABB body = invocation.getArgument(1);
|
||||
return body.maxX <= 16D;
|
||||
});
|
||||
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.5D, 4, 8.5D));
|
||||
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.9D, 4, 8.5D));
|
||||
when(level.noCollision(any(), any(AABB.class))).thenReturn(true);
|
||||
when(level.getChunkSource().hasChunk(1, 0)).thenReturn(false);
|
||||
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 15.9D, 4, 8.5D));
|
||||
EntityDimensions dimensions = player.getDimensions(Pose.STANDING);
|
||||
assertEquals(0, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(15.5D, 4D, 8.5D), new ChunkPos(0, 0)));
|
||||
assertEquals(1, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(15.9D, 4D, 8.5D), new ChunkPos(0, 0)));
|
||||
assertEquals(1, NativeWorldTeleport.warmRadius(dimensions.makeBoundingBox(-16.1D, 4D, -8.5D), new ChunkPos(-2, -1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledStandingBodyStaysInsideActualWorldHeight() {
|
||||
ServerLevel level = level();
|
||||
setSupport(level, Blocks.STONE.defaultBlockState());
|
||||
ServerPlayer player = player();
|
||||
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 13F));
|
||||
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
|
||||
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 12F));
|
||||
assertTrue(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 4, 8.5D));
|
||||
assertFalse(NativeWorldTeleport.isSafeStandingPosition(level, player, 8.5D, 0, 8.5D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconnectDuringChunkWarmupDoesNotTeleportReplacementSession() {
|
||||
MinecraftServer server = mock(MinecraftServer.class, RETURNS_DEEP_STUBS);
|
||||
when(server.isSameThread()).thenReturn(true);
|
||||
ServerLevel level = level();
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, Identifier.parse("iris:test"));
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(level.dimension()).thenReturn(key);
|
||||
when(server.getLevel(key)).thenReturn(level);
|
||||
ServerChunkCache chunks = level.getChunkSource();
|
||||
when(chunks.hasChunk(anyInt(), anyInt())).thenReturn(false);
|
||||
CompletableFuture<Void> loaded = new CompletableFuture<>();
|
||||
doReturn(loaded).when(chunks).addTicketAndLoadWithRadius(any(), any(), anyInt());
|
||||
ServerPlayer original = player();
|
||||
UUID id = UUID.randomUUID();
|
||||
when(original.getUUID()).thenReturn(id);
|
||||
when(server.getPlayerList().getPlayer(id)).thenReturn(original);
|
||||
NativeWorld world = mock(NativeWorld.class);
|
||||
when(world.nativeHandle()).thenReturn(level);
|
||||
CompletableFuture<Boolean> result = NativeWorldTeleport.teleport(NativeProtocolPlayer.fromHandle(original),
|
||||
new NativeWorldTeleport.Destination(NativeModdedServer.fromHandle(server), world, 8.5D, 4D, 8.5D, 0L));
|
||||
assertFalse(result.isDone());
|
||||
ServerPlayer replacement = player();
|
||||
when(server.getPlayerList().getPlayer(id)).thenReturn(replacement);
|
||||
loaded.complete(null);
|
||||
assertFalse(result.join());
|
||||
verify(chunks).removeTicketWithRadius(any(), any(), anyInt());
|
||||
}
|
||||
|
||||
private ServerPlayer player() {
|
||||
ServerPlayer player = mock(ServerPlayer.class);
|
||||
when(player.getDimensions(Pose.STANDING)).thenReturn(EntityDimensions.scalable(0.6F, 1.8F));
|
||||
return player;
|
||||
}
|
||||
|
||||
private ServerLevel level() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
when(level.getChunkSource()).thenReturn(chunks);
|
||||
when(level.getMinY()).thenReturn(0);
|
||||
when(level.getMaxY()).thenReturn(16);
|
||||
when(chunks.hasChunk(anyInt(), anyInt())).thenReturn(true);
|
||||
when(level.noCollision(any(), any(AABB.class))).thenReturn(true);
|
||||
return level;
|
||||
}
|
||||
|
||||
private void setSupport(ServerLevel level, BlockState support) {
|
||||
when(level.getBlockState(any(BlockPos.class))).thenAnswer(invocation -> {
|
||||
BlockPos position = invocation.getArgument(0);
|
||||
return position.getY() == 3 ? support : Blocks.AIR.defaultBlockState();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -379,8 +379,6 @@ List<String> unusedVolmLibClasses = [
|
||||
'art/arcane/volmlib/util/bukkit/Events',
|
||||
'art/arcane/volmlib/util/json/SingleCollectionTypeFactory',
|
||||
'art/arcane/volmlib/util/collection/StateList',
|
||||
'art/arcane/iris/pack/IrisPack',
|
||||
'art/arcane/iris/pack/IrisPackRepository',
|
||||
'art/arcane/iris/world/task/jobs/DownloadJob',
|
||||
'art/arcane/iris/world/task/jobs/JobCollection',
|
||||
'art/arcane/iris/world/task/jobs/ParallelQueueJob',
|
||||
|
||||
@@ -54,7 +54,7 @@ public final class IrisSplashPackScanner {
|
||||
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
JsonElement version = json.get("version");
|
||||
if (version == null || !version.isJsonPrimitive()) {
|
||||
return null;
|
||||
return new SplashPackMetadata(dimName, "unknown");
|
||||
}
|
||||
|
||||
return new SplashPackMetadata(dimName, version.getAsString());
|
||||
|
||||
@@ -96,8 +96,6 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private static final int CHILDREN_SEED_SALT = 2137;
|
||||
|
||||
private final transient AtomicCache<KMap<String, IrisBiomeGeneratorLink>> genCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<KMap<String, Integer>> genCacheMax = new AtomicCache<>();
|
||||
private final transient AtomicCache<KMap<String, Integer>> genCacheMin = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<IrisObjectPlacement>> surfaceObjectsCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<KList<IrisObjectPlacement>> carveObjectsCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<Color> cacheColor = new AtomicCache<>();
|
||||
@@ -415,12 +413,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
return getCustomDerivitives() != null && getCustomDerivitives().isNotEmpty();
|
||||
}
|
||||
|
||||
public double getGenLinkMax(String loadKey, Engine engine) {
|
||||
return IrisBiomeGenLinks.getGenLinkMax(this, loadKey, engine);
|
||||
public double getGenLinkMax(String loadKey) {
|
||||
return IrisBiomeGenLinks.getGenLinkMax(this, loadKey);
|
||||
}
|
||||
|
||||
public double getGenLinkMin(String loadKey, Engine engine) {
|
||||
return IrisBiomeGenLinks.getGenLinkMin(this, loadKey, engine);
|
||||
public double getGenLinkMin(String loadKey) {
|
||||
return IrisBiomeGenLinks.getGenLinkMin(this, loadKey);
|
||||
}
|
||||
|
||||
public IrisBiomeGeneratorLink getGenLink(String loadKey) {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
|
||||
/**
|
||||
@@ -31,24 +30,14 @@ final class IrisBiomeGenLinks {
|
||||
private IrisBiomeGenLinks() {
|
||||
}
|
||||
|
||||
static double getGenLinkMax(IrisBiome biome, String loadKey, Engine engine) {
|
||||
if (loadKey == null || loadKey.isBlank()) {
|
||||
return 0;
|
||||
static double getGenLinkMax(IrisBiome biome, String loadKey) {
|
||||
IrisBiomeGeneratorLink link = getGenLink(biome, loadKey);
|
||||
return link == null ? 0 : link.getMax();
|
||||
}
|
||||
|
||||
Integer v = maxIndex(biome).get(loadKey);
|
||||
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
static double getGenLinkMin(IrisBiome biome, String loadKey, Engine engine) {
|
||||
if (loadKey == null || loadKey.isBlank()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Integer v = minIndex(biome).get(loadKey);
|
||||
|
||||
return v == null ? 0 : v;
|
||||
static double getGenLinkMin(IrisBiome biome, String loadKey) {
|
||||
IrisBiomeGeneratorLink link = getGenLink(biome, loadKey);
|
||||
return link == null ? 0 : link.getMin();
|
||||
}
|
||||
|
||||
static IrisBiomeGeneratorLink getGenLink(IrisBiome biome, String loadKey) {
|
||||
@@ -59,57 +48,6 @@ final class IrisBiomeGenLinks {
|
||||
return linkIndex(biome).get(loadKey);
|
||||
}
|
||||
|
||||
private static KMap<String, Integer> maxIndex(IrisBiome biome) {
|
||||
AtomicCache<KMap<String, Integer>> cache = biome.getGenCacheMax();
|
||||
KMap<String, Integer> cached = cache.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return cache.aquire(() ->
|
||||
{
|
||||
KMap<String, Integer> l = new KMap<>();
|
||||
|
||||
for (IrisBiomeGeneratorLink i : biome.getGenerators()) {
|
||||
String generatorKey = i.getGenerator();
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
l.put(generatorKey, i.getMax());
|
||||
|
||||
}
|
||||
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
private static KMap<String, Integer> minIndex(IrisBiome biome) {
|
||||
AtomicCache<KMap<String, Integer>> cache = biome.getGenCacheMin();
|
||||
KMap<String, Integer> cached = cache.getIfPresent();
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return cache.aquire(() ->
|
||||
{
|
||||
KMap<String, Integer> l = new KMap<>();
|
||||
|
||||
for (IrisBiomeGeneratorLink i : biome.getGenerators()) {
|
||||
String generatorKey = i.getGenerator();
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
l.put(generatorKey, i.getMin());
|
||||
}
|
||||
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
private static KMap<String, IrisBiomeGeneratorLink> linkIndex(IrisBiome biome) {
|
||||
AtomicCache<KMap<String, IrisBiomeGeneratorLink>> cache = biome.getGenCache();
|
||||
KMap<String, IrisBiomeGeneratorLink> cached = cache.getIfPresent();
|
||||
|
||||
@@ -225,7 +225,7 @@ final class IrisBiomeLayerGenerator {
|
||||
|
||||
int sampledDepth = Math.min(maxDepth, cycleLength);
|
||||
for (int depth = 0; depth < sampledDepth; depth++) {
|
||||
int position = Math.floorMod(512L - height - depth, cycleLength);
|
||||
int position = Math.floorMod(512L - height + depth, cycleLength);
|
||||
int layerIndex = findLayer(boundaries, position);
|
||||
IrisBiomePaletteLayer layer = layers.get(layerIndex);
|
||||
int offset = position - boundaries[layerIndex];
|
||||
|
||||
+62
-26
@@ -14,27 +14,31 @@ import java.util.function.Consumer;
|
||||
public final class ConcurrentClockCache<V> {
|
||||
private static final int WAYS = 4;
|
||||
|
||||
private final AtomicReferenceArray<Entry<V>> slots;
|
||||
private final AtomicReferenceArray<CacheSet<V>> slots;
|
||||
private final long sets;
|
||||
private final int capacity;
|
||||
|
||||
public ConcurrentClockCache(int capacity) {
|
||||
if (capacity <= 0) {
|
||||
throw new IllegalArgumentException("Cache capacity must be positive");
|
||||
}
|
||||
int setCount = Math.ceilDiv(capacity, WAYS);
|
||||
this.capacity = Math.multiplyExact(setCount, WAYS);
|
||||
sets = setCount;
|
||||
slots = new AtomicReferenceArray<>(Math.multiplyExact(setCount, WAYS));
|
||||
slots = new AtomicReferenceArray<>(setCount);
|
||||
}
|
||||
|
||||
public int capacity() {
|
||||
return slots.length();
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public V get(long key) {
|
||||
long hash = CacheKey.mix(key);
|
||||
int base = base(hash);
|
||||
CacheSet<V> snapshot = slots.getAcquire(index(CacheKey.mix(key)));
|
||||
if (snapshot == null) {
|
||||
return null;
|
||||
}
|
||||
for (int way = 0; way < WAYS; way++) {
|
||||
Entry<V> entry = slots.getAcquire(base + way);
|
||||
Entry<V> entry = snapshot.get(way);
|
||||
if (entry != null && entry.key == key) {
|
||||
entry.touch();
|
||||
return entry.value;
|
||||
@@ -46,12 +50,23 @@ public final class ConcurrentClockCache<V> {
|
||||
public V putIfAbsent(long key, V value) {
|
||||
Objects.requireNonNull(value, "Cached value");
|
||||
long hash = CacheKey.mix(key);
|
||||
int base = base(hash);
|
||||
int index = index(hash);
|
||||
int hand = (int) (hash >>> 60) & (WAYS - 1);
|
||||
Entry<V> created = null;
|
||||
while (true) {
|
||||
CacheSet<V> snapshot = slots.getAcquire(index);
|
||||
if (snapshot == null) {
|
||||
if (created == null) {
|
||||
created = new Entry<>(key, value);
|
||||
}
|
||||
if (slots.compareAndSet(index, null, new CacheSet<>(created, null, null, null))) {
|
||||
return value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int empty = -1;
|
||||
for (int way = 0; way < WAYS; way++) {
|
||||
Entry<V> entry = slots.getAcquire(base + way);
|
||||
Entry<V> entry = snapshot.get(way);
|
||||
if (entry == null) {
|
||||
if (empty < 0) {
|
||||
empty = way;
|
||||
@@ -61,30 +76,23 @@ public final class ConcurrentClockCache<V> {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
Entry<V> created = new Entry<>(key, value);
|
||||
if (empty >= 0) {
|
||||
if (slots.compareAndSet(base + empty, null, created)) {
|
||||
return value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int victim = -1;
|
||||
Entry<V> victimEntry = null;
|
||||
int victim = empty;
|
||||
for (int step = 0; step < WAYS * 2 && victim < 0; step++) {
|
||||
int way = (hand + step) & (WAYS - 1);
|
||||
Entry<V> entry = slots.getAcquire(base + way);
|
||||
if (entry == null || !entry.referenced) {
|
||||
Entry<V> entry = snapshot.get(way);
|
||||
if (!entry.referenced) {
|
||||
victim = way;
|
||||
victimEntry = entry;
|
||||
} else {
|
||||
entry.referenced = false;
|
||||
}
|
||||
}
|
||||
if (victim < 0) {
|
||||
victim = hand;
|
||||
victimEntry = slots.getAcquire(base + victim);
|
||||
}
|
||||
if (slots.compareAndSet(base + victim, victimEntry, created)) {
|
||||
if (created == null) {
|
||||
created = new Entry<>(key, value);
|
||||
}
|
||||
if (slots.compareAndSet(index, snapshot, snapshot.with(victim, created))) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -92,12 +100,18 @@ public final class ConcurrentClockCache<V> {
|
||||
|
||||
public void forEach(Consumer<? super V> action) {
|
||||
for (int index = 0; index < slots.length(); index++) {
|
||||
Entry<V> entry = slots.getAcquire(index);
|
||||
CacheSet<V> snapshot = slots.getAcquire(index);
|
||||
if (snapshot == null) {
|
||||
continue;
|
||||
}
|
||||
for (int way = 0; way < WAYS; way++) {
|
||||
Entry<V> entry = snapshot.get(way);
|
||||
if (entry != null) {
|
||||
action.accept(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
for (int index = 0; index < slots.length(); index++) {
|
||||
@@ -105,14 +119,36 @@ public final class ConcurrentClockCache<V> {
|
||||
}
|
||||
}
|
||||
|
||||
private int base(long hash) {
|
||||
return (int) (((hash & 0xffffffffL) * sets) >>> 32) * WAYS;
|
||||
private int index(long hash) {
|
||||
return (int) (((hash & 0xffffffffL) * sets) >>> 32);
|
||||
}
|
||||
|
||||
private record CacheSet<V>(Entry<V> first, Entry<V> second, Entry<V> third, Entry<V> fourth) {
|
||||
private Entry<V> get(int way) {
|
||||
return switch (way) {
|
||||
case 0 -> first;
|
||||
case 1 -> second;
|
||||
case 2 -> third;
|
||||
case 3 -> fourth;
|
||||
default -> throw new IndexOutOfBoundsException(way);
|
||||
};
|
||||
}
|
||||
|
||||
private CacheSet<V> with(int way, Entry<V> entry) {
|
||||
return switch (way) {
|
||||
case 0 -> new CacheSet<>(entry, second, third, fourth);
|
||||
case 1 -> new CacheSet<>(first, entry, third, fourth);
|
||||
case 2 -> new CacheSet<>(first, second, entry, fourth);
|
||||
case 3 -> new CacheSet<>(first, second, third, entry);
|
||||
default -> throw new IndexOutOfBoundsException(way);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Entry<V> {
|
||||
private final long key;
|
||||
private final V value;
|
||||
private boolean referenced;
|
||||
private volatile boolean referenced;
|
||||
|
||||
private Entry(long key, V value) {
|
||||
this.key = key;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.generation.cave;
|
||||
|
||||
import art.arcane.iris.pack.value.IrisRange;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -31,12 +33,15 @@ import java.util.List;
|
||||
public final class IrisCaveProfileSampler {
|
||||
private static final double FLOATING_THRESHOLD_BIAS_SCALE = 0.2D;
|
||||
|
||||
private final IrisData data;
|
||||
private final IrisCaveProfile profile;
|
||||
private final CNG baseDensity;
|
||||
private final CNG detailDensity;
|
||||
private final CNG warpDensity;
|
||||
private final RNG thresholdRng;
|
||||
private final CNG thresholdDensity;
|
||||
private final double thresholdMin;
|
||||
private final double thresholdMax;
|
||||
private final double constantThreshold;
|
||||
private final boolean thresholdHasNoise;
|
||||
private final ModuleState[] modules;
|
||||
private final double inverseNormalization;
|
||||
private final double baseWeight;
|
||||
@@ -47,14 +52,19 @@ public final class IrisCaveProfileSampler {
|
||||
private final boolean hasModules;
|
||||
|
||||
public IrisCaveProfileSampler(Engine engine, IrisCaveProfile profile) {
|
||||
this.data = engine.getData();
|
||||
IrisData data = engine.getData();
|
||||
this.profile = profile;
|
||||
List<ModuleState> moduleStates = new ArrayList<>();
|
||||
RNG baseRng = new RNG(engine.getSeedManager().getCarve());
|
||||
this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data);
|
||||
this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data);
|
||||
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
|
||||
this.thresholdRng = baseRng.nextParallelRNG(489_112);
|
||||
IrisStyledRange threshold = profile.getDensityThreshold();
|
||||
thresholdMin = threshold.getMin();
|
||||
thresholdMax = threshold.getMax();
|
||||
thresholdHasNoise = thresholdMin != thresholdMax && !threshold.getStyle().isFlat();
|
||||
thresholdDensity = thresholdHasNoise ? threshold.getStyle().create(baseRng.nextParallelRNG(489_112), data) : null;
|
||||
constantThreshold = thresholdMin == thresholdMax ? thresholdMin : M.lerp(thresholdMin, thresholdMax, 0.5D);
|
||||
this.baseWeight = profile.getBaseWeight();
|
||||
this.detailWeight = profile.getDetailWeight();
|
||||
this.warpStrength = profile.getWarpStrength();
|
||||
@@ -82,7 +92,8 @@ public final class IrisCaveProfileSampler {
|
||||
return false;
|
||||
}
|
||||
|
||||
double threshold = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias();
|
||||
double threshold = (thresholdHasNoise ? thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)
|
||||
: constantThreshold) - profile.getThresholdBias();
|
||||
threshold += floatingThresholdBias(floatingCarveThreshold);
|
||||
return sampleDensity(x, y, z) <= threshold;
|
||||
}
|
||||
|
||||
@@ -98,14 +98,6 @@ final class DecoratorCore {
|
||||
return picked;
|
||||
}
|
||||
|
||||
private static String topHalfValue(String half) {
|
||||
return half.equals("upper") || half.equals("lower") ? "upper" : "top";
|
||||
}
|
||||
|
||||
private static String bottomHalfValue(String half) {
|
||||
return half.equals("upper") || half.equals("lower") ? "lower" : "bottom";
|
||||
}
|
||||
|
||||
static void placeSurfaceSingle(IrisDecorator decorator,
|
||||
int x, int z, int realX, int height, int realZ,
|
||||
Hunk<NativeBlockState> data, RNG rng, IrisData irisData,
|
||||
@@ -150,7 +142,7 @@ final class DecoratorCore {
|
||||
}
|
||||
|
||||
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
|
||||
if (half != null) {
|
||||
if ("upper".equals(half) || "lower".equals(half)) {
|
||||
int lowerY = height + 1;
|
||||
int upperY = height + 2;
|
||||
if (!canPlaceTwoBlockPlant(data, x, z, lowerY, upperY, caveSkipFluid)) {
|
||||
@@ -158,9 +150,9 @@ final class DecoratorCore {
|
||||
}
|
||||
|
||||
try {
|
||||
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
|
||||
NativeBlockState upper = bd.withProperty("half", "upper");
|
||||
NativeBlockState lower = fixFacesForHunk(
|
||||
bd.withProperty("half", bottomHalfValue(half)),
|
||||
bd.withProperty("half", "lower"),
|
||||
data, x, z, realX, lowerY, realZ, mantle);
|
||||
data.set(x, lowerY, z, lower);
|
||||
data.set(x, upperY, z, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(x, upperY, z), true));
|
||||
@@ -228,6 +220,8 @@ final class DecoratorCore {
|
||||
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
|
||||
int placed = 0;
|
||||
boolean hasSpikes = false;
|
||||
NativeBlockState sampledBlock = null;
|
||||
boolean sampledTop = false;
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int y = height + 1 + i;
|
||||
if (y >= data.getHeight()) {
|
||||
@@ -239,9 +233,14 @@ final class DecoratorCore {
|
||||
break;
|
||||
}
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
NativeBlockState block = threshold >= decorator.getTopThreshold()
|
||||
boolean top = threshold >= decorator.getTopThreshold();
|
||||
if (sampledBlock == null || top != sampledTop) {
|
||||
sampledBlock = top
|
||||
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
|
||||
: decorator.pickBlockData(rng, irisData, realX, realZ);
|
||||
sampledTop = top;
|
||||
}
|
||||
NativeBlockState block = sampledBlock;
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
@@ -282,6 +281,8 @@ final class DecoratorCore {
|
||||
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
|
||||
int placed = 0;
|
||||
boolean hasSpikes = false;
|
||||
NativeBlockState sampledBlock = null;
|
||||
boolean sampledTop = false;
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int y = height - i;
|
||||
if (y < 0 || y < minHeight) {
|
||||
@@ -293,9 +294,14 @@ final class DecoratorCore {
|
||||
break;
|
||||
}
|
||||
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
|
||||
NativeBlockState block = threshold >= decorator.getTopThreshold()
|
||||
boolean top = threshold >= decorator.getTopThreshold();
|
||||
if (sampledBlock == null || top != sampledTop) {
|
||||
sampledBlock = top
|
||||
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
|
||||
: decorator.pickBlockData(rng, irisData, realX, realZ);
|
||||
sampledTop = top;
|
||||
}
|
||||
NativeBlockState block = sampledBlock;
|
||||
if (block == null) {
|
||||
break;
|
||||
}
|
||||
@@ -346,7 +352,7 @@ final class DecoratorCore {
|
||||
}
|
||||
|
||||
String half = IrisProceduralBlocks.propertyValue(bd, "half");
|
||||
if (half != null) {
|
||||
if ("upper".equals(half) || "lower".equals(half)) {
|
||||
int lowerY = height + 1;
|
||||
int upperY = height + 2;
|
||||
if (max <= 2 || !canPlaceTwoBlockPlant(data, xf, zf, lowerY, upperY, false)) {
|
||||
@@ -354,8 +360,8 @@ final class DecoratorCore {
|
||||
}
|
||||
|
||||
try {
|
||||
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
|
||||
NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half));
|
||||
NativeBlockState upper = bd.withProperty("half", "upper");
|
||||
NativeBlockState lower = bd.withProperty("half", "lower");
|
||||
data.set(xf, lowerY, zf, IrisProceduralBlocks.normalizeWaterlogging(lower, data.get(xf, lowerY, zf), true));
|
||||
data.set(xf, upperY, zf, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(xf, upperY, zf), true));
|
||||
} catch (Throwable e) {
|
||||
@@ -386,15 +392,22 @@ final class DecoratorCore {
|
||||
|
||||
int placed = 0;
|
||||
boolean hasSpikes = false;
|
||||
NativeBlockState sampledBlock = null;
|
||||
boolean sampledTop = false;
|
||||
for (int i = 0; i < stack; i++) {
|
||||
int h = height + 1 + i;
|
||||
if (h >= height + max || h >= data.getHeight()) {
|
||||
break;
|
||||
}
|
||||
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
|
||||
NativeBlockState bd = threshold >= decorator.getTopThreshold()
|
||||
boolean top = threshold >= decorator.getTopThreshold();
|
||||
if (sampledBlock == null || top != sampledTop) {
|
||||
sampledBlock = top
|
||||
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
|
||||
: decorator.pickBlockData(rng, irisData, realX, realZ);
|
||||
sampledTop = top;
|
||||
}
|
||||
NativeBlockState bd = sampledBlock;
|
||||
if (bd == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -193,20 +193,22 @@ public class IrisDecorator {
|
||||
}
|
||||
|
||||
public NativeBlockState getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) {
|
||||
if (getBlockData(data).isEmpty()) {
|
||||
KList<NativeBlockState> palette = getBlockData(data);
|
||||
if (palette.isEmpty()) {
|
||||
IrisLogging.warnOnce("decorator-empty:" + b.getName(), "Empty Block Data for " + b.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getBlockData(data).size() == 1) {
|
||||
return getBlockData(data).get(0);
|
||||
if (palette.size() == 1) {
|
||||
return palette.get(0);
|
||||
}
|
||||
|
||||
return getVarianceGenerator(rng, data).fit(getBlockData(data), z, y, x); //X and Z must be switched
|
||||
return getVarianceGenerator(rng, data).fit(palette, z, y, x); //X and Z must be switched
|
||||
}
|
||||
|
||||
public NativeBlockState getBlockDataForTop(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) {
|
||||
if (getBlockDataTops(data).isEmpty()) {
|
||||
KList<NativeBlockState> topPalette = getBlockDataTops(data);
|
||||
if (topPalette.isEmpty()) {
|
||||
return getBlockData100(b, rng, x, y, z, data);
|
||||
}
|
||||
|
||||
@@ -214,11 +216,11 @@ public class IrisDecorator {
|
||||
double zz = z / style.getZoom();
|
||||
|
||||
if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { //Exclude y from here
|
||||
if (getBlockData(data).size() == 1) {
|
||||
return getBlockDataTops(data).get(0);
|
||||
if (topPalette.size() == 1) {
|
||||
return topPalette.get(0);
|
||||
}
|
||||
|
||||
return getVarianceGenerator(rng, data).fit(getBlockDataTops(data), z, y, x); //X and Z must be switched
|
||||
return getVarianceGenerator(rng, data).fit(topPalette, z, y, x); //X and Z must be switched
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -146,13 +146,17 @@ public final class IrisProceduralBlocks {
|
||||
}
|
||||
|
||||
public static NativeBlockState normalizeWaterlogging(NativeBlockState state, NativeBlockState existing, boolean enabled) {
|
||||
if (state == null || !hasProperty(state, "waterlogged")) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
String current = propertyValue(state, "waterlogged");
|
||||
if (current == null) {
|
||||
return state;
|
||||
}
|
||||
boolean authored = "true".equals(propertyValue(state, "waterlogged"));
|
||||
boolean authored = "true".equals(current);
|
||||
boolean submerged = existing != null && (existing.isWater() || "true".equals(propertyValue(existing, "waterlogged")));
|
||||
String value = submerged && (enabled || authored) ? "true" : "false";
|
||||
return value.equals(propertyValue(state, "waterlogged")) ? state : state.withProperty("waterlogged", value);
|
||||
return value.equals(current) ? state : state.withProperty("waterlogged", value);
|
||||
}
|
||||
|
||||
public static IrisObject assemble(Map<Vector3i, NativeBlockState> blocks) {
|
||||
|
||||
+1
-6
@@ -111,12 +111,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|
||||
return;
|
||||
}
|
||||
|
||||
int stack = decorator.getHeight(partRNG, realX, realZ, getData());
|
||||
if (decorator.isScaleStack()) {
|
||||
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
|
||||
} else {
|
||||
stack = Math.min(max - height, stack);
|
||||
}
|
||||
int stack = DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height);
|
||||
|
||||
if (stack == 1) {
|
||||
int targetY = height + 1;
|
||||
|
||||
@@ -5,7 +5,6 @@ import art.arcane.iris.generation.hydrology.cave.CavePositionIndex;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveFluidPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveGrottoShape;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
@@ -170,11 +169,8 @@ final class CaveCandidateBuilder {
|
||||
volume,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
dryHeadroom,
|
||||
HydrologyCaveFluidPolicy.REJECT_EXISTING,
|
||||
HydrologyCaveGrottoShape.ELLIPSOID,
|
||||
horizontalRadius,
|
||||
maximumDepth
|
||||
);
|
||||
|
||||
@@ -114,7 +114,7 @@ final class HydrologyOutletPlanner {
|
||||
return selectedOcean;
|
||||
}
|
||||
if (surface) {
|
||||
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (budgets.overridden()) {
|
||||
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
|
||||
}
|
||||
@@ -243,7 +243,7 @@ final class HydrologyOutletPlanner {
|
||||
}
|
||||
sortSurfaceFallbackOutletCandidates(grid, oceanCandidates);
|
||||
sortSurfaceFallbackOutletCandidates(grid, inlandCandidates);
|
||||
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (budgets.overridden()) {
|
||||
HydrologyTileKey key = HydrologyTileKey.fromBlock(grid.ownerMinimumX(), grid.ownerMinimumZ(), grid.ownerSize());
|
||||
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
|
||||
@@ -279,7 +279,7 @@ final class HydrologyOutletPlanner {
|
||||
|
||||
List<OutletCandidate> limitSurfaceOutlets(HydrologyTileKey key, HydrologySampledGrid grid,
|
||||
List<OutletCandidate> candidates, boolean coastal) {
|
||||
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (!budgets.overridden()) {
|
||||
return coastal ? limitOutletsByType(candidates, planner.settings.outlets().maximumCoastalPerTile())
|
||||
: limitOutlets(candidates, planner.settings.outlets().maximumPerTile());
|
||||
|
||||
@@ -12,6 +12,7 @@ final class HydrologySampledGrid {
|
||||
private final int spacing;
|
||||
private final List<HydrologyGridNode> nodes;
|
||||
private volatile HydrologyDrainageGraph drainage;
|
||||
private volatile SurfaceBudgetSnapshot surfaceBudgets;
|
||||
|
||||
HydrologySampledGrid(int minimumX, int minimumZ, int ownerMinimumX, int ownerMinimumZ,
|
||||
int ownerSize, int width, int spacing, List<HydrologyGridNode> nodes) {
|
||||
@@ -77,6 +78,21 @@ final class HydrologySampledGrid {
|
||||
return nodes.get(index);
|
||||
}
|
||||
|
||||
HydrologySurfaceBudgets surfaceBudgets(HydrologyPlannerSettings.Source sources) {
|
||||
SurfaceBudgetSnapshot current = surfaceBudgets;
|
||||
if (current != null && current.sources().equals(sources)) {
|
||||
return current.budgets();
|
||||
}
|
||||
synchronized (this) {
|
||||
current = surfaceBudgets;
|
||||
if (current == null || !current.sources().equals(sources)) {
|
||||
current = new SurfaceBudgetSnapshot(sources, HydrologySurfaceBudgets.sample(this, sources));
|
||||
surfaceBudgets = current;
|
||||
}
|
||||
return current.budgets();
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyGridNode nodeAt(int gridX, int gridZ) {
|
||||
if (gridX < 0 || gridZ < 0 || gridX >= width || gridZ >= width) {
|
||||
return null;
|
||||
@@ -102,4 +118,7 @@ final class HydrologySampledGrid {
|
||||
return x >= ownerMinimumX && x < ownerMinimumX + ownerSize
|
||||
&& z >= ownerMinimumZ && z < ownerMinimumZ + ownerSize;
|
||||
}
|
||||
|
||||
private record SurfaceBudgetSnapshot(HydrologyPlannerSettings.Source sources, HydrologySurfaceBudgets budgets) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ final class HydrologySourcePlanner {
|
||||
return SourceSelection.empty(surface);
|
||||
}
|
||||
long sourceSalt = surface ? SURFACE_SOURCE_SALT : UNDERGROUND_SOURCE_SALT;
|
||||
HydrologySurfaceBudgets surfaceBudgets = surface ? HydrologySurfaceBudgets.sample(grid, sourceSettings) : null;
|
||||
HydrologySurfaceBudgets surfaceBudgets = surface ? grid.surfaceBudgets(sourceSettings) : null;
|
||||
ArrayList<SourceCandidate> candidates = new ArrayList<>();
|
||||
boolean hasRequiredCandidate = false;
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
record CaveBoundaryResult(Set<CavePosition> sealGuards, HydrologyCaveRejection rejection) {
|
||||
static CaveBoundaryResult accepted(Set<CavePosition> sealGuards) {
|
||||
return new CaveBoundaryResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(sealGuards)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveBoundaryResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveBoundaryResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
record CaveComponentResult(Set<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CaveComponentResult accepted(Set<CavePosition> positions) {
|
||||
return new CaveComponentResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveComponentResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveComponentResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
record CaveGrottoResult(Set<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CaveGrottoResult accepted(Set<CavePosition> positions) {
|
||||
return new CaveGrottoResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveGrottoResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveGrottoResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CavePathResult(List<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CavePathResult accepted(List<CavePosition> positions) {
|
||||
return new CavePathResult(List.copyOf(positions), HydrologyCaveRejection.NONE);
|
||||
}
|
||||
|
||||
static CavePathResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CavePathResult(List.of(), rejection);
|
||||
}
|
||||
}
|
||||
-809
@@ -1,809 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyCaveChamberPlanner {
|
||||
private final HydrologyCaveContainmentPlanner planner;
|
||||
|
||||
HydrologyCaveChamberPlanner(HydrologyCaveContainmentPlanner planner) {
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
HydrologyCavePlan planGrottoOrClosedComponent(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
|
||||
if (planner.isFluidReachable(targetVoxel, settings)) {
|
||||
return planClosedComponent(view, source, settings, throat);
|
||||
}
|
||||
if (targetVoxel == CaveVoxel.LAVA) {
|
||||
return planner.rejected(source, HydrologyCaveRejection.LAVA_CONTACT);
|
||||
}
|
||||
if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) {
|
||||
return planner.rejected(source, HydrologyCaveRejection.INCOMPATIBLE_FLUID);
|
||||
}
|
||||
return planGeneratedGrotto(view, source, settings, throat);
|
||||
}
|
||||
|
||||
HydrologyCavePlan planWaterfallPool(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
if (!view.isOpenToSurface(source.target())) {
|
||||
return planGrottoOrClosedComponent(view, source, settings, throat);
|
||||
}
|
||||
HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
|
||||
if (dryThroatRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, dryThroatRejection);
|
||||
}
|
||||
HydrologyCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat);
|
||||
if (shaftRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, shaftRejection);
|
||||
}
|
||||
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
|
||||
if (!planner.isFluidReachable(targetVoxel, settings)) {
|
||||
return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings));
|
||||
}
|
||||
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
addThroatActions(actions, throat, source);
|
||||
addSealGuards(view, source, actions);
|
||||
return planner.accepted(view, source, actions);
|
||||
}
|
||||
|
||||
HydrologyCavePlan planClosedComponent(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
|
||||
if (dryThroatRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, dryThroatRejection);
|
||||
}
|
||||
HydrologyCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat);
|
||||
if (waterfallRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, waterfallRejection);
|
||||
}
|
||||
|
||||
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
|
||||
if (!planner.isFluidReachable(targetVoxel, settings)) {
|
||||
return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings));
|
||||
}
|
||||
|
||||
CaveComponentResult component = resolveClosedComponent(view, source, settings, throat);
|
||||
if (component.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, component.rejection());
|
||||
}
|
||||
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
addThroatActions(actions, throat, source);
|
||||
for (CavePosition position : component.positions()) {
|
||||
actions.put(position, HydrologyCaveAction.WET_SOURCE);
|
||||
}
|
||||
addSealGuards(view, source, actions);
|
||||
return planner.accepted(view, source, actions);
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateDryThroatContacts(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
Set<CavePosition> throatPositions = Set.copyOf(throat);
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, neighbor);
|
||||
if (voxel == CaveVoxel.LAVA) {
|
||||
return HydrologyCaveRejection.LAVA_CONTACT;
|
||||
}
|
||||
if (voxel == CaveVoxel.COMPATIBLE_FLUID) {
|
||||
return HydrologyCaveRejection.EXISTING_FLUID;
|
||||
}
|
||||
if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) {
|
||||
return HydrologyCaveRejection.INCOMPATIBLE_FLUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateWaterfallShaft(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
if (source.mode() != HydrologyCaveMode.WATERFALL_POOL) {
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
Set<CavePosition> throatPositions = Set.copyOf(throat);
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, neighbor);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return HydrologyCaveRejection.WATERFALL_SHAFT_OPEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCavePlan planGeneratedGrotto(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
CaveGrottoResult grotto = buildGrotto(source, settings);
|
||||
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, grotto.rejection());
|
||||
}
|
||||
Set<CavePosition> chamber = grotto.positions();
|
||||
|
||||
Set<CavePosition> carve = new LinkedHashSet<>(chamber.size() + throat.size());
|
||||
carve.addAll(chamber);
|
||||
carve.addAll(throat);
|
||||
HydrologyCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve);
|
||||
if (carveRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, carveRejection);
|
||||
}
|
||||
|
||||
CaveBoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve);
|
||||
if (boundary.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, boundary.rejection());
|
||||
}
|
||||
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
addChamberActions(actions, chamber, source.waterHeadY());
|
||||
addThroatActions(actions, throat, source);
|
||||
for (CavePosition position : boundary.sealGuards()) {
|
||||
actions.put(position, HydrologyCaveAction.SEAL_GUARD);
|
||||
}
|
||||
return planner.accepted(view, source, actions);
|
||||
}
|
||||
|
||||
HydrologyCavePlan planDeepPool(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
CaveGrottoResult grotto = buildGrotto(source, settings);
|
||||
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, grotto.rejection());
|
||||
}
|
||||
Set<CavePosition> chamber = grotto.positions();
|
||||
Set<CavePosition> carve = new LinkedHashSet<>(chamber.size() + throat.size());
|
||||
carve.addAll(chamber);
|
||||
carve.addAll(throat);
|
||||
|
||||
HydrologyCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve);
|
||||
if (carveRejection != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, carveRejection);
|
||||
}
|
||||
CaveBoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve);
|
||||
if (boundary.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, boundary.rejection());
|
||||
}
|
||||
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
addChamberActions(actions, chamber, source.waterHeadY());
|
||||
addThroatActions(actions, throat, source);
|
||||
for (CavePosition position : boundary.sealGuards()) {
|
||||
actions.put(position, HydrologyCaveAction.SEAL_GUARD);
|
||||
}
|
||||
return planner.accepted(view, source, actions);
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateDeepPoolCarve(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
for (CavePosition position : carve) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, position);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (voxel == CaveVoxel.SOLID) {
|
||||
continue;
|
||||
}
|
||||
if (position.y() > source.waterHeadY()
|
||||
&& voxel == CaveVoxel.CAVE_AIR
|
||||
&& !view.isOpenToSurface(position)) {
|
||||
continue;
|
||||
}
|
||||
return HydrologyCaveRejection.GROTTO_INTERSECTION;
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
CaveBoundaryResult validateDeepPoolBoundary(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
Set<CavePosition> guards = new LinkedHashSet<>();
|
||||
for (CavePosition position : carve) {
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (carve.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY);
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return CaveBoundaryResult.rejected(boundsRejection);
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, neighbor);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return CaveBoundaryResult.rejected(hazard);
|
||||
}
|
||||
if (voxel == CaveVoxel.SOLID) {
|
||||
guards.add(neighbor);
|
||||
continue;
|
||||
}
|
||||
if (neighbor.y() > source.waterHeadY()
|
||||
&& voxel == CaveVoxel.CAVE_AIR
|
||||
&& !view.isOpenToSurface(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN);
|
||||
}
|
||||
}
|
||||
return CaveBoundaryResult.accepted(guards);
|
||||
}
|
||||
|
||||
CaveComponentResult resolveClosedComponent(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat
|
||||
) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> queued = new LinkedHashSet<>();
|
||||
int visitedCount = 0;
|
||||
|
||||
queue.add(source.target());
|
||||
queued.add(source.target());
|
||||
HydrologyCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued);
|
||||
if (seedRejection != HydrologyCaveRejection.NONE) {
|
||||
return CaveComponentResult.rejected(seedRejection);
|
||||
}
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
CavePosition position = queue.remove();
|
||||
HydrologyCaveRejection positionRejection = validateReachablePosition(view, source, settings, position);
|
||||
if (positionRejection != HydrologyCaveRejection.NONE) {
|
||||
return CaveComponentResult.rejected(positionRejection);
|
||||
}
|
||||
if (++visitedCount > settings.maxFloodVolume()) {
|
||||
return CaveComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
HydrologyCaveRejection neighborRejection = inspectReachableNeighbor(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
neighbor,
|
||||
queue,
|
||||
queued
|
||||
);
|
||||
if (neighborRejection != HydrologyCaveRejection.NONE) {
|
||||
return CaveComponentResult.rejected(neighborRejection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CaveComponentResult.accepted(queued);
|
||||
}
|
||||
|
||||
HydrologyCaveRejection addThroatContacts(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> throat,
|
||||
Queue<CavePosition> queue,
|
||||
Set<CavePosition> queued
|
||||
) {
|
||||
for (CavePosition position : throat) {
|
||||
if (position.y() > source.waterHeadY()) {
|
||||
continue;
|
||||
}
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
HydrologyCaveRejection rejection = inspectReachableNeighbor(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
neighbor,
|
||||
queue,
|
||||
queued
|
||||
);
|
||||
if (rejection != HydrologyCaveRejection.NONE) {
|
||||
return rejection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection inspectReachableNeighbor(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
CavePosition position,
|
||||
Queue<CavePosition> queue,
|
||||
Set<CavePosition> queued
|
||||
) {
|
||||
if (position.y() > source.waterHeadY()) {
|
||||
return inspectAboveHeadNeighbor(view, source, position);
|
||||
}
|
||||
if (!view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
|
||||
CaveVoxel voxel = planner.voxelAt(view, position);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (!planner.isFluidReachable(voxel, settings)) {
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
if (queued.add(position)) {
|
||||
queue.add(position);
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection inspectAboveHeadNeighbor(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
CavePosition position
|
||||
) {
|
||||
if (isInletOpening(source, position) || !view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, position);
|
||||
return switch (voxel) {
|
||||
case LAVA -> HydrologyCaveRejection.LAVA_CONTACT;
|
||||
case COMPATIBLE_FLUID -> HydrologyCaveRejection.EXISTING_FLUID;
|
||||
case INCOMPATIBLE_FLUID -> HydrologyCaveRejection.INCOMPATIBLE_FLUID;
|
||||
default -> HydrologyCaveRejection.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateReachablePosition(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
if (view.isOpenToSurface(position)) {
|
||||
return HydrologyCaveRejection.OPEN_SURFACE;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, position);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
return planner.isFluidReachable(voxel, settings)
|
||||
? HydrologyCaveRejection.NONE
|
||||
: HydrologyCaveRejection.NO_CAVE_TARGET;
|
||||
}
|
||||
|
||||
CavePathResult buildThroat(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings
|
||||
) {
|
||||
CavePosition entry = source.entry();
|
||||
CavePosition target = source.target();
|
||||
int deltaX = target.x() - entry.x();
|
||||
int deltaY = target.y() - entry.y();
|
||||
int deltaZ = target.z() - entry.z();
|
||||
int movesX = Math.abs(deltaX);
|
||||
int movesY = Math.abs(deltaY);
|
||||
int movesZ = Math.abs(deltaZ);
|
||||
int length = movesX + movesY + movesZ;
|
||||
if (length > settings.maxThroatLength()) {
|
||||
return CavePathResult.rejected(HydrologyCaveRejection.THROAT_LIMIT);
|
||||
}
|
||||
|
||||
int stepX = Integer.signum(deltaX);
|
||||
int stepY = Integer.signum(deltaY);
|
||||
int stepZ = Integer.signum(deltaZ);
|
||||
int usedX = 0;
|
||||
int usedY = 0;
|
||||
int usedZ = 0;
|
||||
CavePosition current = entry;
|
||||
List<CavePosition> positions = new ArrayList<>(length + 1);
|
||||
|
||||
while (true) {
|
||||
HydrologyCaveRejection positionRejection = validateThroatPosition(view, source, settings, current);
|
||||
if (positionRejection != HydrologyCaveRejection.NONE) {
|
||||
return CavePathResult.rejected(positionRejection);
|
||||
}
|
||||
positions.add(current);
|
||||
if (current.equals(target)) {
|
||||
return expandThroat(view, source, settings, positions);
|
||||
}
|
||||
|
||||
int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ);
|
||||
if (axis == 0) {
|
||||
current = current.offset(stepX, 0, 0);
|
||||
usedX++;
|
||||
} else if (axis == 1) {
|
||||
current = current.offset(0, stepY, 0);
|
||||
usedY++;
|
||||
} else {
|
||||
current = current.offset(0, 0, stepZ);
|
||||
usedZ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CavePathResult expandThroat(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
List<CavePosition> centerline
|
||||
) {
|
||||
int radius = settings.throatRadius();
|
||||
int extent = radius - 1;
|
||||
int radiusSquared = radius * radius;
|
||||
Set<CavePosition> expanded = new LinkedHashSet<>();
|
||||
for (CavePosition center : centerline) {
|
||||
for (int dx = -extent; dx <= extent; dx++) {
|
||||
for (int dy = -extent; dy <= extent; dy++) {
|
||||
for (int dz = -extent; dz <= extent; dz++) {
|
||||
if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) {
|
||||
continue;
|
||||
}
|
||||
CavePosition position = center.offset(dx, dy, dz);
|
||||
if (position.y() > source.entry().y()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyCaveRejection rejection = validateThroatPosition(view, source, settings, position);
|
||||
if (rejection != HydrologyCaveRejection.NONE) {
|
||||
return CavePathResult.rejected(rejection);
|
||||
}
|
||||
expanded.add(position);
|
||||
if (expanded.size() > settings.maxFloodVolume()) {
|
||||
return CavePathResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return CavePathResult.accepted(List.copyOf(expanded));
|
||||
}
|
||||
|
||||
int selectNextAxis(
|
||||
long sourceId,
|
||||
int movesX,
|
||||
int movesY,
|
||||
int movesZ,
|
||||
int usedX,
|
||||
int usedY,
|
||||
int usedZ
|
||||
) {
|
||||
double scoreX = nextAxisScore(movesX, usedX);
|
||||
double scoreY = nextAxisScore(movesY, usedY);
|
||||
double scoreZ = nextAxisScore(movesZ, usedZ);
|
||||
double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ));
|
||||
int tieOffset = Math.floorMod(sourceId, 3);
|
||||
for (int offset = 0; offset < 3; offset++) {
|
||||
int axis = (tieOffset + offset) % 3;
|
||||
double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ;
|
||||
if (score == minimum) {
|
||||
return axis;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No remaining throat axis");
|
||||
}
|
||||
|
||||
double nextAxisScore(int moves, int used) {
|
||||
if (used >= moves) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
return ((2D * used) + 1D) / moves;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateThroatPosition(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
return planner.rejectionForHazard(planner.voxelAt(view, position), settings);
|
||||
}
|
||||
|
||||
CaveGrottoResult buildGrotto(HydrologyCaveSource source, HydrologyCavePlannerSettings settings) {
|
||||
int horizontalRadius = settings.grottoHorizontalRadius();
|
||||
int verticalRadius = settings.grottoVerticalRadius();
|
||||
Set<CavePosition> candidates = new LinkedHashSet<>();
|
||||
|
||||
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
|
||||
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
|
||||
for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) {
|
||||
if (settings.grottoShape().contains(source, settings, dx, dy, dz)) {
|
||||
candidates.add(source.target().offset(dx, dy, dz));
|
||||
if (candidates.size() > settings.maxFloodVolume()) {
|
||||
return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates.add(source.target());
|
||||
for (int offset = 1; offset <= settings.dryHeadroom(); offset++) {
|
||||
CavePosition headroom = new CavePosition(
|
||||
source.target().x(), source.waterHeadY() + offset, source.target().z());
|
||||
if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) {
|
||||
return CaveGrottoResult.rejected(HydrologyCaveRejection.DRY_HEADROOM_LIMIT);
|
||||
}
|
||||
candidates.add(headroom);
|
||||
if (candidates.size() > settings.maxFloodVolume()) {
|
||||
return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
|
||||
}
|
||||
}
|
||||
return CaveGrottoResult.accepted(connectedGrotto(source.target(), candidates));
|
||||
}
|
||||
|
||||
Set<CavePosition> connectedGrotto(CavePosition target, Set<CavePosition> candidates) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> connected = new LinkedHashSet<>();
|
||||
queue.add(target);
|
||||
connected.add(target);
|
||||
while (!queue.isEmpty()) {
|
||||
CavePosition position = queue.remove();
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (candidates.contains(neighbor) && connected.add(neighbor)) {
|
||||
queue.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection validateGeneratedCarve(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
for (CavePosition position : carve) {
|
||||
if (!view.isInWorld(position)) {
|
||||
return HydrologyCaveRejection.WORLD_BOUNDARY;
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return boundsRejection;
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, position);
|
||||
if (isGeneratedInletCarve(view, source, settings, position, voxel)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return hazard;
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return HydrologyCaveRejection.GROTTO_INTERSECTION;
|
||||
}
|
||||
}
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
CaveBoundaryResult validateGeneratedBoundary(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
Set<CavePosition> carve
|
||||
) {
|
||||
Set<CavePosition> guards = new LinkedHashSet<>();
|
||||
for (CavePosition position : carve) {
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (carve.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (isInletOpening(source, neighbor)
|
||||
|| isGeneratedInletOpening(view, source, settings, neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (!view.isInWorld(neighbor)) {
|
||||
return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY);
|
||||
}
|
||||
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
|
||||
if (boundsRejection != HydrologyCaveRejection.NONE) {
|
||||
return CaveBoundaryResult.rejected(boundsRejection);
|
||||
}
|
||||
CaveVoxel voxel = planner.voxelAt(view, neighbor);
|
||||
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
|
||||
if (hazard != HydrologyCaveRejection.NONE) {
|
||||
return CaveBoundaryResult.rejected(hazard);
|
||||
}
|
||||
if (voxel != CaveVoxel.SOLID) {
|
||||
return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN);
|
||||
}
|
||||
guards.add(neighbor);
|
||||
}
|
||||
}
|
||||
return CaveBoundaryResult.accepted(guards);
|
||||
}
|
||||
|
||||
void addChamberActions(
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
Collection<CavePosition> positions,
|
||||
int waterHeadY
|
||||
) {
|
||||
for (CavePosition position : positions) {
|
||||
HydrologyCaveAction action = position.y() <= waterHeadY
|
||||
? HydrologyCaveAction.WET_SOURCE
|
||||
: HydrologyCaveAction.DRY_AIR;
|
||||
actions.put(position, action);
|
||||
}
|
||||
}
|
||||
|
||||
void addThroatActions(
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
Collection<CavePosition> throat,
|
||||
HydrologyCaveSource source
|
||||
) {
|
||||
for (CavePosition position : throat) {
|
||||
HydrologyCaveAction action;
|
||||
if (position.y() <= source.waterHeadY()) {
|
||||
action = HydrologyCaveAction.WET_SOURCE;
|
||||
} else if (source.mode() == HydrologyCaveMode.WATERFALL_POOL
|
||||
|| source.mode() == HydrologyCaveMode.GENERATED_GROTTO) {
|
||||
action = HydrologyCaveAction.FALLING_FLUID;
|
||||
} else {
|
||||
action = HydrologyCaveAction.DRY_AIR;
|
||||
}
|
||||
actions.put(position, action);
|
||||
}
|
||||
}
|
||||
|
||||
void addSealGuards(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
Map<CavePosition, HydrologyCaveAction> actions
|
||||
) {
|
||||
Set<CavePosition> guards = new LinkedHashSet<>();
|
||||
for (CavePosition position : List.copyOf(actions.keySet())) {
|
||||
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
|
||||
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
|
||||
if (actions.containsKey(neighbor)
|
||||
|| isInletOpening(source, neighbor)
|
||||
|| !view.isInWorld(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
if (planner.voxelAt(view, neighbor) == CaveVoxel.SOLID) {
|
||||
guards.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (CavePosition guard : guards) {
|
||||
actions.put(guard, HydrologyCaveAction.SEAL_GUARD);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isInletOpening(HydrologyCaveSource source, CavePosition position) {
|
||||
return position.equals(source.entry().offset(0, 1, 0));
|
||||
}
|
||||
|
||||
boolean isGeneratedInletCarve(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
CavePosition position,
|
||||
CaveVoxel voxel
|
||||
) {
|
||||
if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) {
|
||||
return false;
|
||||
}
|
||||
int extent = Math.max(0, settings.throatRadius() - 1);
|
||||
long deltaX = (long) position.x() - source.entry().x();
|
||||
long deltaZ = (long) position.z() - source.entry().z();
|
||||
return position.y() >= source.entry().y() - extent
|
||||
&& position.y() <= source.entry().y()
|
||||
&& deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent
|
||||
&& view.isOpenToSurface(position);
|
||||
}
|
||||
|
||||
boolean isGeneratedInletOpening(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
CavePosition position
|
||||
) {
|
||||
int radius = Math.max(1, settings.throatRadius());
|
||||
long deltaX = (long) position.x() - source.entry().x();
|
||||
long deltaZ = (long) position.z() - source.entry().z();
|
||||
return position.y() >= source.entry().y()
|
||||
&& position.y() <= source.entry().y() + 1
|
||||
&& deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius
|
||||
&& view.isOpenToSurface(position);
|
||||
}
|
||||
}
|
||||
-133
@@ -29,91 +29,6 @@ public final class HydrologyCaveContainmentPlanner {
|
||||
|
||||
static final Comparator<HydrologyCaveSource> SOURCE_PRIORITY =
|
||||
HydrologyCaveConflictPolicy.sourcePriority();
|
||||
final HydrologyCaveChamberPlanner chambers;
|
||||
|
||||
public HydrologyCaveContainmentPlanner() {
|
||||
this.chambers = new HydrologyCaveChamberPlanner(this);
|
||||
}
|
||||
|
||||
public HydrologyCavePlan plan(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings
|
||||
) {
|
||||
Objects.requireNonNull(view);
|
||||
Objects.requireNonNull(source);
|
||||
Objects.requireNonNull(settings);
|
||||
|
||||
HydrologyCaveRejection sourceRejection = validateSource(source);
|
||||
if (sourceRejection != HydrologyCaveRejection.NONE) {
|
||||
return rejected(source, sourceRejection);
|
||||
}
|
||||
|
||||
CavePathResult throat = chambers.buildThroat(view, source, settings);
|
||||
if (throat.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return rejected(source, throat.rejection());
|
||||
}
|
||||
|
||||
return switch (source.mode()) {
|
||||
case CLOSED_COMPONENT -> chambers.planClosedComponent(view, source, settings, throat.positions());
|
||||
case GENERATED_GROTTO -> chambers.planGeneratedGrotto(view, source, settings, throat.positions());
|
||||
case GROTTO_OR_CLOSED_COMPONENT -> chambers.planGrottoOrClosedComponent(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
throat.positions()
|
||||
);
|
||||
case WATERFALL_POOL -> chambers.planWaterfallPool(
|
||||
view,
|
||||
source,
|
||||
settings,
|
||||
throat.positions()
|
||||
);
|
||||
case DEEP_POOL -> chambers.planDeepPool(view, source, settings, throat.positions());
|
||||
};
|
||||
}
|
||||
|
||||
public HydrologyCavePlanningResult planAll(
|
||||
CaveVoxelView view,
|
||||
Collection<HydrologyCaveSource> sources,
|
||||
HydrologyCavePlannerSettings settings
|
||||
) {
|
||||
Objects.requireNonNull(view);
|
||||
Objects.requireNonNull(sources);
|
||||
Objects.requireNonNull(settings);
|
||||
|
||||
List<HydrologyCaveSource> orderedSources = new ArrayList<>(sources);
|
||||
orderedSources.sort(SOURCE_PRIORITY);
|
||||
List<HydrologyCavePlan> plans = new ArrayList<>(orderedSources.size());
|
||||
Map<CavePosition, HydrologyCaveAction> combinedActions = new LinkedHashMap<>();
|
||||
Map<CavePosition, HydrologyCaveSource> claimedBy = new HashMap<>();
|
||||
Map<CavePosition, CaveVoxelPrecondition> combinedPreconditions = new LinkedHashMap<>();
|
||||
|
||||
for (HydrologyCaveSource source : orderedSources) {
|
||||
HydrologyCavePlan candidate = plan(view, source, settings);
|
||||
if (!candidate.accepted()) {
|
||||
plans.add(candidate);
|
||||
continue;
|
||||
}
|
||||
OptionalLong winnerSourceId = findWinningSourceId(
|
||||
candidate.actions().keySet(),
|
||||
claimedBy
|
||||
);
|
||||
if (winnerSourceId.isPresent()) {
|
||||
plans.add(rejectedOverlap(source, winnerSourceId.getAsLong()));
|
||||
} else {
|
||||
plans.add(candidate);
|
||||
combinedActions.putAll(candidate.actions());
|
||||
combinedPreconditions.putAll(candidate.baselinePreconditions());
|
||||
}
|
||||
for (CavePosition position : candidate.actions().keySet()) {
|
||||
claimedBy.putIfAbsent(position, source);
|
||||
}
|
||||
}
|
||||
|
||||
return new HydrologyCavePlanningResult(plans, combinedActions, combinedPreconditions);
|
||||
}
|
||||
|
||||
public HydrologyCavePlanningResult validateAll(
|
||||
CaveVoxelView view,
|
||||
Collection<HydrologyCaveCandidate> candidates
|
||||
@@ -698,14 +613,6 @@ public final class HydrologyCaveContainmentPlanner {
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection rejectionForTarget(
|
||||
CaveVoxel voxel,
|
||||
HydrologyCavePlannerSettings settings
|
||||
) {
|
||||
HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings);
|
||||
return hazard == HydrologyCaveRejection.NONE ? HydrologyCaveRejection.NO_CAVE_TARGET : hazard;
|
||||
}
|
||||
|
||||
HydrologyCaveRejection rejectionForHazard(
|
||||
CaveVoxel voxel,
|
||||
HydrologyCavePlannerSettings settings
|
||||
@@ -722,50 +629,10 @@ public final class HydrologyCaveContainmentPlanner {
|
||||
};
|
||||
}
|
||||
|
||||
boolean isFluidReachable(CaveVoxel voxel, HydrologyCavePlannerSettings settings) {
|
||||
return voxel == CaveVoxel.CAVE_AIR
|
||||
|| (voxel == CaveVoxel.COMPATIBLE_FLUID
|
||||
&& settings.existingFluidPolicy() != HydrologyCaveFluidPolicy.REJECT_EXISTING)
|
||||
|| (voxel == CaveVoxel.INCOMPATIBLE_FLUID
|
||||
&& settings.existingFluidPolicy() == HydrologyCaveFluidPolicy.REPLACE_CONTAINED);
|
||||
}
|
||||
|
||||
CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) {
|
||||
return Objects.requireNonNull(view.voxelAt(position));
|
||||
}
|
||||
|
||||
OptionalLong findWinningSourceId(
|
||||
Set<CavePosition> positions,
|
||||
Map<CavePosition, HydrologyCaveSource> claimedBy
|
||||
) {
|
||||
HydrologyCaveSource winner = null;
|
||||
for (CavePosition position : positions) {
|
||||
HydrologyCaveSource contender = claimedBy.get(position);
|
||||
if (contender == null) {
|
||||
continue;
|
||||
}
|
||||
if (winner == null || SOURCE_PRIORITY.compare(contender, winner) < 0) {
|
||||
winner = contender;
|
||||
}
|
||||
}
|
||||
return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId());
|
||||
}
|
||||
|
||||
HydrologyCavePlan accepted(
|
||||
CaveVoxelView view,
|
||||
HydrologyCaveSource source,
|
||||
Map<CavePosition, HydrologyCaveAction> actions
|
||||
) {
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>(actions.size());
|
||||
for (CavePosition position : actions.keySet()) {
|
||||
preconditions.put(
|
||||
position,
|
||||
new CaveVoxelPrecondition(voxelAt(view, position), view.isOpenToSurface(position))
|
||||
);
|
||||
}
|
||||
return accepted(source, actions, preconditions);
|
||||
}
|
||||
|
||||
HydrologyCavePlan accepted(
|
||||
HydrologyCaveSource source,
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology.cave;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyCaveGrottoShape {
|
||||
HydrologyCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
|
||||
double horizontalRadius = settings.grottoHorizontalRadius();
|
||||
double verticalRadius = settings.grottoVerticalRadius();
|
||||
double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius))
|
||||
+ ((double) dy * dy / (verticalRadius * verticalRadius))
|
||||
+ ((double) dz * dz / (horizontalRadius * horizontalRadius));
|
||||
return normalized <= 1D;
|
||||
};
|
||||
|
||||
boolean contains(
|
||||
HydrologyCaveSource source,
|
||||
HydrologyCavePlannerSettings settings,
|
||||
int offsetX,
|
||||
int offsetY,
|
||||
int offsetZ
|
||||
);
|
||||
}
|
||||
-61
@@ -6,72 +6,17 @@ public record HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
int maxThroatLength,
|
||||
int throatRadius,
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
int dryHeadroom,
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy,
|
||||
HydrologyCaveGrottoShape grottoShape,
|
||||
int maxClosedComponentHorizontalRadius,
|
||||
int maxClosedComponentDepth
|
||||
) {
|
||||
public static final int MAXIMUM_PLANNED_MUTATIONS = 262_144;
|
||||
|
||||
public HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
int maxThroatLength,
|
||||
int throatRadius,
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
int dryHeadroom,
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy,
|
||||
HydrologyCaveGrottoShape grottoShape
|
||||
) {
|
||||
this(
|
||||
maxHorizontalRadius,
|
||||
maxDepth,
|
||||
maxFloodVolume,
|
||||
maxThroatLength,
|
||||
throatRadius,
|
||||
grottoHorizontalRadius,
|
||||
grottoVerticalRadius,
|
||||
dryHeadroom,
|
||||
existingFluidPolicy,
|
||||
grottoShape,
|
||||
maxHorizontalRadius,
|
||||
maxDepth
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyCavePlannerSettings(
|
||||
int maxHorizontalRadius,
|
||||
int maxDepth,
|
||||
int maxFloodVolume,
|
||||
int maxThroatLength,
|
||||
int grottoHorizontalRadius,
|
||||
int grottoVerticalRadius,
|
||||
HydrologyCaveFluidPolicy existingFluidPolicy
|
||||
) {
|
||||
this(
|
||||
maxHorizontalRadius,
|
||||
maxDepth,
|
||||
maxFloodVolume,
|
||||
maxThroatLength,
|
||||
1,
|
||||
grottoHorizontalRadius,
|
||||
grottoVerticalRadius,
|
||||
0,
|
||||
existingFluidPolicy,
|
||||
HydrologyCaveGrottoShape.ELLIPSOID
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyCavePlannerSettings {
|
||||
Objects.requireNonNull(existingFluidPolicy);
|
||||
Objects.requireNonNull(grottoShape);
|
||||
if (maxHorizontalRadius < 1) {
|
||||
throw new IllegalArgumentException("maxHorizontalRadius must be positive");
|
||||
}
|
||||
@@ -81,12 +26,6 @@ public record HydrologyCavePlannerSettings(
|
||||
if (maxFloodVolume < 1) {
|
||||
throw new IllegalArgumentException("maxFloodVolume must be positive");
|
||||
}
|
||||
if (maxThroatLength < 1) {
|
||||
throw new IllegalArgumentException("maxThroatLength must be positive");
|
||||
}
|
||||
if (throatRadius < 1) {
|
||||
throw new IllegalArgumentException("throatRadius must be positive");
|
||||
}
|
||||
if (grottoHorizontalRadius < 1) {
|
||||
throw new IllegalArgumentException("grottoHorizontalRadius must be positive");
|
||||
}
|
||||
|
||||
+2
-2
@@ -376,7 +376,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
private double naturalHeight(CacheStripe stripe, long packed, int blockX, int blockZ) {
|
||||
synchronized (stripe) {
|
||||
double cached = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(cached) || stripe.naturalHeights.containsKey(packed)) {
|
||||
if (!Double.isNaN(cached)) {
|
||||
return cached;
|
||||
}
|
||||
TerrainBasis cachedBasis = stripe.bases.getAndMoveToLast(packed);
|
||||
@@ -392,7 +392,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
}
|
||||
synchronized (stripe) {
|
||||
double existing = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(existing) || stripe.naturalHeights.containsKey(packed)) {
|
||||
if (!Double.isNaN(existing)) {
|
||||
return existing;
|
||||
}
|
||||
stripe.naturalHeights.putAndMoveToLast(packed, sampled);
|
||||
|
||||
+31
-18
@@ -9,7 +9,8 @@ import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
|
||||
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
@@ -149,8 +150,9 @@ public final class ErosionFieldCompiler {
|
||||
boolean[] basin = prepared.basin();
|
||||
double[] bendOffsets = prepared.bendOffsets();
|
||||
double[][] blendWidth = prepared.blendWidth();
|
||||
Long2IntOpenHashMap nearest = geometry.nearest;
|
||||
Long2DoubleOpenHashMap distance = geometry.distance;
|
||||
Long2IntOpenHashMap indices = geometry.indices;
|
||||
IntArrayList stations = geometry.stations;
|
||||
DoubleArrayList distance = geometry.distance;
|
||||
if (!geometry.prepared) {
|
||||
for (int station = 0; station < count; station++) {
|
||||
int radius = prepared.radii()[station];
|
||||
@@ -172,14 +174,22 @@ public final class ErosionFieldCompiler {
|
||||
continue;
|
||||
}
|
||||
long key = RiverFootprint.pack(cellX, cellZ);
|
||||
int existing = nearest.get(key);
|
||||
int index = indices.get(key);
|
||||
int existing = index < 0 ? -1 : stations.getInt(index);
|
||||
double existingDistance = index < 0 ? 0D : distance.getDouble(index);
|
||||
// A cell on a station's cross-section row ties between the two segments meeting
|
||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||
if (existing < 0
|
||||
|| cellDistance < distance.get(key) - EPSILON
|
||||
|| Math.abs(cellDistance - distance.get(key)) <= EPSILON && station > existing) {
|
||||
nearest.put(key, station);
|
||||
distance.put(key, cellDistance);
|
||||
|| cellDistance < existingDistance - EPSILON
|
||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||
if (index < 0) {
|
||||
indices.put(key, stations.size());
|
||||
stations.add(station);
|
||||
distance.add(cellDistance);
|
||||
} else {
|
||||
stations.set(index, station);
|
||||
distance.set(index, cellDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,17 +199,18 @@ public final class ErosionFieldCompiler {
|
||||
int oceanStart = oceanStart(centerline, valley, terminal);
|
||||
int inletStart = prepared.inletStart();
|
||||
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(wetOnly
|
||||
? Math.min(nearest.size(), Math.max(16, count * 8)) : nearest.size());
|
||||
? Math.min(indices.size(), Math.max(16, count * 8)) : indices.size());
|
||||
LongArrayList wetKeys = new LongArrayList();
|
||||
LongIterator candidates = nearest.keySet().iterator();
|
||||
long[] coordinates = new long[Math.min(nearest.size(), HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE)];
|
||||
LongIterator candidates = indices.keySet().iterator();
|
||||
long[] coordinates = new long[Math.min(indices.size(), HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE)];
|
||||
double[] outlines = new double[coordinates.length];
|
||||
while (candidates.hasNext()) {
|
||||
int pageSize = 0;
|
||||
while (candidates.hasNext() && pageSize < coordinates.length) {
|
||||
long key = candidates.nextLong();
|
||||
int station = nearest.get(key);
|
||||
double cellDistance = distance.get(key);
|
||||
int geometryIndex = indices.get(key);
|
||||
int station = stations.getInt(geometryIndex);
|
||||
double cellDistance = distance.getDouble(geometryIndex);
|
||||
int cellX = RiverFootprint.unpackX(key);
|
||||
int cellZ = RiverFootprint.unpackZ(key);
|
||||
int head = valley.head()[station];
|
||||
@@ -231,8 +242,9 @@ public final class ErosionFieldCompiler {
|
||||
HydrologyTerrainSample[] samples = sampler.sampleBatch(coordinates, pageSize);
|
||||
for (int index = 0; index < pageSize; index++) {
|
||||
long key = coordinates[index];
|
||||
int station = nearest.get(key);
|
||||
double cellDistance = distance.get(key);
|
||||
int geometryIndex = indices.get(key);
|
||||
int station = stations.getInt(geometryIndex);
|
||||
double cellDistance = distance.getDouble(geometryIndex);
|
||||
int cellX = RiverFootprint.unpackX(key);
|
||||
int cellZ = RiverFootprint.unpackZ(key);
|
||||
int head = valley.head()[station];
|
||||
@@ -898,12 +910,13 @@ public final class ErosionFieldCompiler {
|
||||
}
|
||||
|
||||
private static final class RasterGeometry {
|
||||
private final Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
|
||||
private final Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
|
||||
private final Long2IntOpenHashMap indices = new Long2IntOpenHashMap();
|
||||
private final IntArrayList stations = new IntArrayList();
|
||||
private final DoubleArrayList distance = new DoubleArrayList();
|
||||
private boolean prepared;
|
||||
|
||||
private RasterGeometry() {
|
||||
nearest.defaultReturnValue(-1);
|
||||
indices.defaultReturnValue(-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,36 +22,27 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.Terrain3DColumn;
|
||||
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainCell;
|
||||
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import lombok.Data;
|
||||
import lombok.AccessLevel;
|
||||
@@ -61,9 +52,7 @@ import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.function.Supplier;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
@@ -71,7 +60,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
import static art.arcane.iris.generation.mantle.EngineMantle.AIR;
|
||||
@@ -215,122 +203,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
|
||||
Set<IrisPosition> returnset = new HashSet<>();
|
||||
int ceilrad = (int) Math.ceil(radius);
|
||||
double r2 = Math.pow(radius, 2);
|
||||
|
||||
for (IrisPosition v : vset) {
|
||||
int tipx = v.getX();
|
||||
int tipy = v.getY();
|
||||
int tipz = v.getZ();
|
||||
|
||||
for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) {
|
||||
for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) {
|
||||
for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) {
|
||||
if (hypot(loopx - tipx, loopy - tipy, loopz - tipz) <= r2) {
|
||||
returnset.add(new IrisPosition(loopx, loopy, loopz));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnset;
|
||||
}
|
||||
|
||||
static Set<IrisPosition> getHollowed(Set<IrisPosition> vset) {
|
||||
PositionLookup lookup = PositionLookup.of(vset);
|
||||
Set<IrisPosition> returnset = new KSet<>();
|
||||
for (IrisPosition v : vset) {
|
||||
int x = v.getX();
|
||||
int y = v.getY();
|
||||
int z = v.getZ();
|
||||
if (!(lookup.contains(x + 1, y, z)
|
||||
&& lookup.contains(x - 1, y, z)
|
||||
&& lookup.contains(x, y + 1, z)
|
||||
&& lookup.contains(x, y - 1, z)
|
||||
&& lookup.contains(x, y, z + 1)
|
||||
&& lookup.contains(x, y, z - 1))) {
|
||||
returnset.add(v);
|
||||
}
|
||||
}
|
||||
return returnset;
|
||||
}
|
||||
|
||||
private static double hypot(double x, double y, double z) {
|
||||
return Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2);
|
||||
}
|
||||
|
||||
record PositionLookup(LongOpenHashSet keys, Set<IrisPosition> positions, int originX, int originY, int originZ) {
|
||||
private static final int AXIS_BITS = 21;
|
||||
private static final long AXIS_LIMIT = 1L << AXIS_BITS;
|
||||
|
||||
static PositionLookup of(Set<IrisPosition> positions) {
|
||||
if (positions.isEmpty()) {
|
||||
return new PositionLookup(new LongOpenHashSet(), positions, 0, 0, 0);
|
||||
}
|
||||
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (IrisPosition position : positions) {
|
||||
minX = Math.min(minX, position.getX());
|
||||
minY = Math.min(minY, position.getY());
|
||||
minZ = Math.min(minZ, position.getZ());
|
||||
maxX = Math.max(maxX, position.getX());
|
||||
maxY = Math.max(maxY, position.getY());
|
||||
maxZ = Math.max(maxZ, position.getZ());
|
||||
}
|
||||
|
||||
if ((long) maxX - minX >= AXIS_LIMIT - 2L
|
||||
|| (long) maxY - minY >= AXIS_LIMIT - 2L
|
||||
|| (long) maxZ - minZ >= AXIS_LIMIT - 2L) {
|
||||
return new PositionLookup(null, positions, 0, 0, 0);
|
||||
}
|
||||
|
||||
LongOpenHashSet keys = new LongOpenHashSet(positions.size());
|
||||
for (IrisPosition position : positions) {
|
||||
keys.add(key(position.getX() - minX, position.getY() - minY, position.getZ() - minZ));
|
||||
}
|
||||
return new PositionLookup(keys, positions, minX, minY, minZ);
|
||||
}
|
||||
|
||||
boolean contains(int x, int y, int z) {
|
||||
if (keys == null) {
|
||||
return positions.contains(new IrisPosition(x, y, z));
|
||||
}
|
||||
|
||||
long dx = (long) x - originX;
|
||||
long dy = (long) y - originY;
|
||||
long dz = (long) z - originZ;
|
||||
if ((dx | dy | dz) < 0L || dx >= AXIS_LIMIT || dy >= AXIS_LIMIT || dz >= AXIS_LIMIT) {
|
||||
return false;
|
||||
}
|
||||
return keys.contains(key(dx, dy, dz));
|
||||
}
|
||||
|
||||
private static long key(long x, long y, long z) {
|
||||
return (x << (AXIS_BITS * 2)) | (y << AXIS_BITS) | z;
|
||||
}
|
||||
}
|
||||
|
||||
private static double lengthSq(double x, double y, double z) {
|
||||
return (x * x) + (y * y) + (z * z);
|
||||
}
|
||||
|
||||
private static double lengthSq(double x, double z) {
|
||||
return (x * x) + (z * z);
|
||||
}
|
||||
|
||||
public <T> void setDataWarped(int x, int y, int z, T t, RNG rng, IrisData data, IrisGeneratorStyle style) {
|
||||
setData((int) Math.round(style.warp(rng, data, x, x, y, -z)),
|
||||
(int) Math.round(style.warp(rng, data, y, z, -x, y)),
|
||||
(int) Math.round(style.warp(rng, data, z, -y, z, x)), t);
|
||||
}
|
||||
|
||||
public ObjectPlacementCapture captureObjectPlacement(ObjectContinuationBundle.PlacementKey key) {
|
||||
if (objectPlacementCapture.get() != null) {
|
||||
throw new IllegalStateException("Object placement capture is already active");
|
||||
@@ -376,6 +248,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return;
|
||||
}
|
||||
if (t instanceof NativeBlockState) {
|
||||
clearTile(matter, x, y, z);
|
||||
clearDeferredPlacement(matter, x, y, z);
|
||||
}
|
||||
Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t);
|
||||
@@ -671,69 +544,21 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return carvedColumn;
|
||||
}
|
||||
|
||||
public boolean restorePrerequisiteData(int x, int y, int z, Class<?> type) {
|
||||
void restoreData(int x, int y, int z, Class<?> type, Object value) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!allowsWrite(x, z)
|
||||
|| y < 0
|
||||
|| y >= mantle.getWorldHeight()
|
||||
|| !isPreObjectType(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
int section = y >> 4;
|
||||
if (chunk == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronized (chunk) {
|
||||
if (!chunk.exists(section)) {
|
||||
return false;
|
||||
}
|
||||
Matter matter = chunk.get(section);
|
||||
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
|
||||
if (cell == null || !cell.captures(type)) {
|
||||
return false;
|
||||
}
|
||||
restoreRaw(matter, x, y, z, type, cell.original(type));
|
||||
recordObjectValue(x, y, z, type, cell.original(type));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean restorePrerequisiteCell(int x, int y, int z) {
|
||||
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
|
||||
int section = y >> 4;
|
||||
if (chunk == null) {
|
||||
return false;
|
||||
throw new IllegalStateException("Object rollback exceeded its prepared Mantle radius at "
|
||||
+ x + "," + y + "," + z);
|
||||
}
|
||||
|
||||
synchronized (chunk) {
|
||||
if (!chunk.exists(section)) {
|
||||
return false;
|
||||
Matter matter = chunk.get(y >> 4);
|
||||
if (matter != null) {
|
||||
restoreRaw(matter, x, y, z, type, value);
|
||||
}
|
||||
Matter matter = chunk.get(section);
|
||||
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
|
||||
if (cell == null) {
|
||||
return false;
|
||||
}
|
||||
if (cell.blockCaptured()) {
|
||||
restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block());
|
||||
recordObjectValue(x, y, z, NativeBlockState.class, cell.block());
|
||||
}
|
||||
if (cell.stringCaptured()) {
|
||||
restoreRaw(matter, x, y, z, String.class, cell.string());
|
||||
recordObjectValue(x, y, z, String.class, cell.string());
|
||||
}
|
||||
if (cell.cavernCaptured()) {
|
||||
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
|
||||
recordObjectValue(x, y, z, MatterCavern.class, cell.cavern());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,6 +603,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return hydrology != null && hydrology.protectsPlacement();
|
||||
}
|
||||
|
||||
private static void clearTile(Matter matter, int x, int y, int z) {
|
||||
MatterSlice<TileWrapper> slice = matter.getSlice(TileWrapper.class);
|
||||
if (slice != null) {
|
||||
slice.set(x & 15, y & 15, z & 15, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
|
||||
MatterSlice<Identifier> slice = matter.getSlice(Identifier.class);
|
||||
if (slice != null) {
|
||||
@@ -803,6 +635,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
clearTile(matter, x, y, z);
|
||||
MatterSlice<NativeBlockState> blockSlice = matter.slice(NativeBlockState.class);
|
||||
MatterSlice<Identifier> identifierSlice = matter.slice(Identifier.class);
|
||||
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
|
||||
@@ -1102,479 +935,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
return getEngineMantle().getEngine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a sphere into the mantle
|
||||
*
|
||||
* @param cx the center x
|
||||
* @param cy the center y
|
||||
* @param cz the center z
|
||||
* @param radius the radius of this sphere
|
||||
* @param fill should it be filled? or just the outer shell?
|
||||
* @param data the data to set
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setSphere(int cx, int cy, int cz, double radius, boolean fill, T data) {
|
||||
setElipsoid(cx, cy, cz, radius, radius, radius, fill, data);
|
||||
}
|
||||
|
||||
public <T> void setElipsoid(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data) {
|
||||
setElipsoidFunction(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data);
|
||||
}
|
||||
|
||||
public <T> void setElipsoidWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data, RNG rng, IrisData idata, IrisGeneratorStyle style) {
|
||||
setElipsoidFunctionWarped(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data, rng, idata, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an elipsoid into the mantle
|
||||
*
|
||||
* @param cx the center x
|
||||
* @param cy the center y
|
||||
* @param cz the center z
|
||||
* @param rx the x radius
|
||||
* @param ry the y radius
|
||||
* @param rz the z radius
|
||||
* @param fill should it be filled or just the outer shell?
|
||||
* @param data the data to set
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setElipsoidFunction(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3<Integer, Integer, Integer, T> data) {
|
||||
rx += 0.5;
|
||||
ry += 0.5;
|
||||
rz += 0.5;
|
||||
final double invRadiusX = 1 / rx;
|
||||
final double invRadiusY = 1 / ry;
|
||||
final double invRadiusZ = 1 / rz;
|
||||
final int ceilRadiusX = (int) Math.ceil(rx);
|
||||
final int ceilRadiusY = (int) Math.ceil(ry);
|
||||
final int ceilRadiusZ = (int) Math.ceil(rz);
|
||||
double nextXn = 0;
|
||||
|
||||
forX:
|
||||
for (int x = 0; x <= ceilRadiusX; ++x) {
|
||||
final double xn = nextXn;
|
||||
nextXn = (x + 1) * invRadiusX;
|
||||
double nextYn = 0;
|
||||
forY:
|
||||
for (int y = 0; y <= ceilRadiusY; ++y) {
|
||||
final double yn = nextYn;
|
||||
nextYn = (y + 1) * invRadiusY;
|
||||
double nextZn = 0;
|
||||
for (int z = 0; z <= ceilRadiusZ; ++z) {
|
||||
final double zn = nextZn;
|
||||
nextZn = (z + 1) * invRadiusZ;
|
||||
|
||||
double distanceSq = lengthSq(xn, yn, zn);
|
||||
if (distanceSq > 1) {
|
||||
if (z == 0) {
|
||||
if (y == 0) {
|
||||
break forX;
|
||||
}
|
||||
break forY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!fill) {
|
||||
if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
setData(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz));
|
||||
setData(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz));
|
||||
setData(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz));
|
||||
setData(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz));
|
||||
setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz));
|
||||
setData(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz));
|
||||
setData(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz));
|
||||
setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz));
|
||||
setData(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void setElipsoidFunctionWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3<Integer, Integer, Integer, T> data, RNG rng, IrisData idata, IrisGeneratorStyle style) {
|
||||
rx += 0.5;
|
||||
ry += 0.5;
|
||||
rz += 0.5;
|
||||
final double invRadiusX = 1 / rx;
|
||||
final double invRadiusY = 1 / ry;
|
||||
final double invRadiusZ = 1 / rz;
|
||||
final int ceilRadiusX = (int) Math.ceil(rx);
|
||||
final int ceilRadiusY = (int) Math.ceil(ry);
|
||||
final int ceilRadiusZ = (int) Math.ceil(rz);
|
||||
double nextXn = 0;
|
||||
|
||||
forX:
|
||||
for (int x = 0; x <= ceilRadiusX; ++x) {
|
||||
final double xn = nextXn;
|
||||
nextXn = (x + 1) * invRadiusX;
|
||||
double nextYn = 0;
|
||||
forY:
|
||||
for (int y = 0; y <= ceilRadiusY; ++y) {
|
||||
final double yn = nextYn;
|
||||
nextYn = (y + 1) * invRadiusY;
|
||||
double nextZn = 0;
|
||||
for (int z = 0; z <= ceilRadiusZ; ++z) {
|
||||
final double zn = nextZn;
|
||||
nextZn = (z + 1) * invRadiusZ;
|
||||
|
||||
double distanceSq = lengthSq(xn, yn, zn);
|
||||
if (distanceSq > 1) {
|
||||
if (z == 0) {
|
||||
if (y == 0) {
|
||||
break forX;
|
||||
}
|
||||
break forY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!fill) {
|
||||
if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
setDataWarped(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz), rng, idata, style);
|
||||
setDataWarped(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz), rng, idata, style);
|
||||
setDataWarped(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz), rng, idata, style);
|
||||
setDataWarped(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz), rng, idata, style);
|
||||
setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style);
|
||||
setDataWarped(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz), rng, idata, style);
|
||||
setDataWarped(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz), rng, idata, style);
|
||||
setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style);
|
||||
setDataWarped(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz), rng, idata, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cuboid of data in the mantle
|
||||
*
|
||||
* @param x1 the min x
|
||||
* @param y1 the min y
|
||||
* @param z1 the min z
|
||||
* @param x2 the max x
|
||||
* @param y2 the max y
|
||||
* @param z2 the max z
|
||||
* @param data the data to set
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setCuboid(int x1, int y1, int z1, int x2, int y2, int z2, T data) {
|
||||
int j, k;
|
||||
|
||||
for (int i = x1; i <= x2; i++) {
|
||||
for (j = x1; j <= x2; j++) {
|
||||
for (k = x1; k <= x2; k++) {
|
||||
setData(i, j, k, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a pyramid of data in the mantle
|
||||
*
|
||||
* @param cx the center x
|
||||
* @param cy the base y
|
||||
* @param cz the center z
|
||||
* @param data the data to set
|
||||
* @param size the size of the pyramid (width of base & height)
|
||||
* @param filled should it be filled or hollow
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public <T> void setPyramid(int cx, int cy, int cz, T data, int size, boolean filled) {
|
||||
int height = size;
|
||||
|
||||
for (int y = 0; y <= height; ++y) {
|
||||
size--;
|
||||
for (int x = 0; x <= size; ++x) {
|
||||
for (int z = 0; z <= size; ++z) {
|
||||
if ((filled && z <= size && x <= size) || z == size || x == size) {
|
||||
setData(x + cx, y + cy, z + cz, data);
|
||||
setData(-x + cx, y + cy, z + cz, data);
|
||||
setData(x + cx, y + cy, -z + cz, data);
|
||||
setData(-x + cx, y + cy, -z + cz, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a 3d line
|
||||
*
|
||||
* @param a the first point
|
||||
* @param b the second point
|
||||
* @param radius the radius
|
||||
* @param filled hollow or filled?
|
||||
* @param data the data
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setLine(IrisPosition a, IrisPosition b, double radius, boolean filled, T data) {
|
||||
setLine(ImmutableList.of(a, b), radius, filled, data);
|
||||
}
|
||||
|
||||
public <T> void setLine(List<IrisPosition> vectors, double radius, boolean filled, T data) {
|
||||
setLineConsumer(vectors, radius, filled, (_x, _y, _z) -> data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lines for points
|
||||
*
|
||||
* @param vectors the points
|
||||
* @param radius the radius
|
||||
* @param filled hollow or filled?
|
||||
* @param data the data to set
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setLineConsumer(List<IrisPosition> vectors, double radius, boolean filled, Function3<Integer, Integer, Integer, T> data) {
|
||||
Set<IrisPosition> vset = cleanup(vectors);
|
||||
vset = getBallooned(vset, radius);
|
||||
|
||||
if (!filled) {
|
||||
vset = getHollowed(vset);
|
||||
}
|
||||
|
||||
setConsumer(vset, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lines for points
|
||||
*
|
||||
* @param vectors the points
|
||||
* @param radius the radius
|
||||
* @param filled hollow or filled?
|
||||
* @param data the data to set
|
||||
* @param <T> the type of data to apply to the mantle
|
||||
*/
|
||||
public <T> void setNoiseMasked(List<IrisPosition> vectors, double radius, double threshold, CNG shape, Set<IrisPosition> masks, boolean filled, Function3<Integer, Integer, Integer, T> data) {
|
||||
Set<IrisPosition> vset = cleanup(vectors);
|
||||
vset = masks == null ? getBallooned(vset, radius) : getMasked(vset, masks, radius);
|
||||
vset.removeIf(p -> shape.noise(p.getX(), p.getY(), p.getZ()) < threshold);
|
||||
|
||||
if (!filled) {
|
||||
vset = getHollowed(vset);
|
||||
}
|
||||
|
||||
setConsumer(vset, data);
|
||||
}
|
||||
|
||||
static Set<IrisPosition> getMasked(Set<IrisPosition> vectors, Set<IrisPosition> masks, double radius) {
|
||||
Set<IrisPosition> vset = new KSet<>();
|
||||
int ceil = (int) Math.ceil(radius);
|
||||
double r2 = Math.pow(radius, 2);
|
||||
PositionLookup maskLookup = PositionLookup.of(masks);
|
||||
|
||||
for (IrisPosition v : vectors) {
|
||||
int tipX = v.getX();
|
||||
int tipY = v.getY();
|
||||
int tipZ = v.getZ();
|
||||
|
||||
for (int x = -ceil; x <= ceil; x++) {
|
||||
for (int y = -ceil; y <= ceil; y++) {
|
||||
for (int z = -ceil; z <= ceil; z++) {
|
||||
if (hypot(x, y, z) > r2 || !maskLookup.contains(x, y, z))
|
||||
continue;
|
||||
vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vset;
|
||||
}
|
||||
|
||||
private static Set<IrisPosition> cleanup(List<IrisPosition> vectors) {
|
||||
Set<IrisPosition> vset = new KSet<>();
|
||||
|
||||
for (int i = 0; vectors.size() != 0 && i < vectors.size() - 1; i++) {
|
||||
IrisPosition pos1 = vectors.get(i);
|
||||
IrisPosition pos2 = vectors.get(i + 1);
|
||||
int x1 = pos1.getX();
|
||||
int y1 = pos1.getY();
|
||||
int z1 = pos1.getZ();
|
||||
int x2 = pos2.getX();
|
||||
int y2 = pos2.getY();
|
||||
int z2 = pos2.getZ();
|
||||
int tipx = x1;
|
||||
int tipy = y1;
|
||||
int tipz = z1;
|
||||
int dx = Math.abs(x2 - x1);
|
||||
int dy = Math.abs(y2 - y1);
|
||||
int dz = Math.abs(z2 - z1);
|
||||
|
||||
if (dx + dy + dz == 0) {
|
||||
vset.add(new IrisPosition(tipx, tipy, tipz));
|
||||
continue;
|
||||
}
|
||||
|
||||
int dMax = Math.max(Math.max(dx, dy), dz);
|
||||
if (dMax == dx) {
|
||||
for (int domstep = 0; domstep <= dx; domstep++) {
|
||||
tipx = x1 + domstep * (x2 - x1 > 0 ? 1 : -1);
|
||||
tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dx) * (y2 - y1 > 0 ? 1 : -1));
|
||||
tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dx) * (z2 - z1 > 0 ? 1 : -1));
|
||||
|
||||
vset.add(new IrisPosition(tipx, tipy, tipz));
|
||||
}
|
||||
} else if (dMax == dy) {
|
||||
for (int domstep = 0; domstep <= dy; domstep++) {
|
||||
tipy = y1 + domstep * (y2 - y1 > 0 ? 1 : -1);
|
||||
tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dy) * (x2 - x1 > 0 ? 1 : -1));
|
||||
tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dy) * (z2 - z1 > 0 ? 1 : -1));
|
||||
|
||||
vset.add(new IrisPosition(tipx, tipy, tipz));
|
||||
}
|
||||
} else /* if (dMax == dz) */ {
|
||||
for (int domstep = 0; domstep <= dz; domstep++) {
|
||||
tipz = z1 + domstep * (z2 - z1 > 0 ? 1 : -1);
|
||||
tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dz) * (y2 - y1 > 0 ? 1 : -1));
|
||||
tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dz) * (x2 - x1 > 0 ? 1 : -1));
|
||||
|
||||
vset.add(new IrisPosition(tipx, tipy, tipz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cylinder in the mantle
|
||||
*
|
||||
* @param cx the center x
|
||||
* @param cy the base y
|
||||
* @param cz the center z
|
||||
* @param data the data to set
|
||||
* @param radius the radius
|
||||
* @param height the height of the cyl
|
||||
* @param filled filled or not
|
||||
*/
|
||||
public <T> void setCylinder(int cx, int cy, int cz, T data, double radius, int height, boolean filled) {
|
||||
setCylinder(cx, cy, cz, data, radius, radius, height, filled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cylinder in the mantle
|
||||
*
|
||||
* @param cx the center x
|
||||
* @param cy the base y
|
||||
* @param cz the center z
|
||||
* @param data the data to set
|
||||
* @param radiusX the x radius
|
||||
* @param radiusZ the z radius
|
||||
* @param height the height of this cyl
|
||||
* @param filled filled or hollow?
|
||||
*/
|
||||
public <T> void setCylinder(int cx, int cy, int cz, T data, double radiusX, double radiusZ, int height, boolean filled) {
|
||||
int affected = 0;
|
||||
radiusX += 0.5;
|
||||
radiusZ += 0.5;
|
||||
|
||||
if (height == 0) {
|
||||
return;
|
||||
} else if (height < 0) {
|
||||
height = -height;
|
||||
cy = cy - height;
|
||||
}
|
||||
|
||||
if (cy < 0) {
|
||||
cy = 0;
|
||||
} else if (cy + height - 1 > getMantle().getWorldHeight()) {
|
||||
height = getMantle().getWorldHeight() - cy + 1;
|
||||
}
|
||||
|
||||
final double invRadiusX = 1 / radiusX;
|
||||
final double invRadiusZ = 1 / radiusZ;
|
||||
final int ceilRadiusX = (int) Math.ceil(radiusX);
|
||||
final int ceilRadiusZ = (int) Math.ceil(radiusZ);
|
||||
double nextXn = 0;
|
||||
|
||||
forX:
|
||||
for (int x = 0; x <= ceilRadiusX; ++x) {
|
||||
final double xn = nextXn;
|
||||
nextXn = (x + 1) * invRadiusX;
|
||||
double nextZn = 0;
|
||||
for (int z = 0; z <= ceilRadiusZ; ++z) {
|
||||
final double zn = nextZn;
|
||||
nextZn = (z + 1) * invRadiusZ;
|
||||
double distanceSq = lengthSq(xn, zn);
|
||||
|
||||
if (distanceSq > 1) {
|
||||
if (z == 0) {
|
||||
break forX;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!filled) {
|
||||
if (lengthSq(nextXn, zn) <= 1 && lengthSq(xn, nextZn) <= 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
setData(cx + x, cy + y, cz + z, data);
|
||||
setData(cx + -x, cy + y, cz + z, data);
|
||||
setData(cx + x, cy + y, cz + -z, data);
|
||||
setData(cx + -x, cy + y, cz + -z, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void set(IrisPosition pos, T data) {
|
||||
try {
|
||||
setData(pos.getX(), pos.getY(), pos.getZ(), data);
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
// Reached per position while an object writes past the edge of the writer window, which is
|
||||
// how a large object is clipped. Nothing here is actionable from outside the engine.
|
||||
IrisLogging.debug("No set? " + data.toString() + " for " + pos.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void set(List<IrisPosition> positions, T data) {
|
||||
for (IrisPosition i : positions) {
|
||||
set(i, data);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void set(Set<IrisPosition> positions, T data) {
|
||||
for (IrisPosition i : positions) {
|
||||
set(i, data);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void setConsumer(Set<IrisPosition> positions, Function3<Integer, Integer, Integer, T> data) {
|
||||
for (IrisPosition i : positions) {
|
||||
set(i, data.apply(i.getX(), i.getY(), i.getZ()));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isWithin(Vector pos) {
|
||||
return isWithin(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ());
|
||||
}
|
||||
|
||||
public boolean isWithin(int x, int y, int z) {
|
||||
int cx = x >> 4;
|
||||
int cz = z >> 4;
|
||||
|
||||
if (y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cx >= this.x - radius && cx <= this.x + radius
|
||||
&& cz >= this.z - radius && cz <= this.z + radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (int index = 0; index < window.length(); index++) {
|
||||
@@ -1689,6 +1049,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
maximumX = Math.max(maximumX, x);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
if (type == NativeBlockState.class) {
|
||||
MatterSlice<TileWrapper> tiles = fragment.getSlice(TileWrapper.class);
|
||||
if (tiles != null) {
|
||||
tiles.set(x & 15, y, z & 15, null);
|
||||
}
|
||||
fragment.slice(Identifier.class).set(x & 15, y, z & 15, null);
|
||||
fragment.slice(type).set(x & 15, y, z & 15, value == null ? AIR.get() : value);
|
||||
} else {
|
||||
|
||||
+5
@@ -8,6 +8,7 @@ import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -51,6 +52,10 @@ public final class ObjectContinuationPersistence {
|
||||
|
||||
public static void put(Matter payload, int x, int y, int z, Object value) {
|
||||
if (value instanceof NativeBlockState state) {
|
||||
MatterSlice<TileWrapper> tiles = payload.getSlice(TileWrapper.class);
|
||||
if (tiles != null) {
|
||||
tiles.set(x, y, z, null);
|
||||
}
|
||||
String customKey = state.deferredPlacementKey();
|
||||
NativeBlockState base = state.placementBaseState();
|
||||
boolean custom = state.isCustom() && customKey != null && base != null;
|
||||
|
||||
+30
-26
@@ -99,12 +99,19 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
captureOriginal(originals, mutation.key());
|
||||
if (mutation.key().type() == NativeBlockState.class) {
|
||||
captureOriginal(originals, new DataKey(
|
||||
mutation.key().x(),
|
||||
mutation.key().y(),
|
||||
mutation.key().z(),
|
||||
Identifier.class
|
||||
));
|
||||
TileWrapper tile = writer.getDataIfPresent(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
|
||||
if (tile != null) {
|
||||
originals.putIfAbsent(new DataKey(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
|
||||
}
|
||||
Identifier identifier = writer.getDataIfPresent(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class);
|
||||
if (identifier != null || mutation instanceof CustomBlockMutation) {
|
||||
originals.putIfAbsent(new DataKey(
|
||||
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class),
|
||||
identifier == null ? CLEARED : identifier);
|
||||
}
|
||||
}
|
||||
mutation.apply(writer);
|
||||
}
|
||||
@@ -148,6 +155,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
Identifier identifier = Identifier.fromString(placementKey);
|
||||
OverlayCell cell = writableCell(x, y, z);
|
||||
cell.block = baseState;
|
||||
cell.tile = CLEARED;
|
||||
cell.identifier = identifier;
|
||||
mutations.add(replayed != null
|
||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
|
||||
@@ -243,6 +251,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
OverlayCell cell = writableCell(x, y, z);
|
||||
if (data instanceof NativeBlockState) {
|
||||
cell.block = data;
|
||||
cell.tile = CLEARED;
|
||||
cell.identifier = CLEARED;
|
||||
} else {
|
||||
cell.put(type, data);
|
||||
@@ -325,15 +334,15 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
return cell;
|
||||
}
|
||||
|
||||
private Object prerequisiteOrCleared(int x, int y, int z, Class<?> type) {
|
||||
Object value = writer.getPrerequisiteDataIfPresent(x, y, z, type);
|
||||
private Object publishedOrCleared(int x, int y, int z, Class<?> type) {
|
||||
Object value = writer.getDataIfPresent(x, y, z, type);
|
||||
return value == null ? CLEARED : value;
|
||||
}
|
||||
|
||||
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
|
||||
originals.computeIfAbsent(
|
||||
key,
|
||||
candidate -> prerequisiteOrCleared(
|
||||
candidate -> publishedOrCleared(
|
||||
candidate.x(),
|
||||
candidate.y(),
|
||||
candidate.z(),
|
||||
@@ -383,36 +392,26 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
|
||||
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
|
||||
entries.sort((first, second) -> Boolean.compare(
|
||||
second.getKey().type() == NativeBlockState.class,
|
||||
first.getKey().type() == NativeBlockState.class
|
||||
));
|
||||
for (Map.Entry<DataKey, Object> entry : entries) {
|
||||
for (int index = entries.size() - 1; index >= 0; index--) {
|
||||
Map.Entry<DataKey, Object> entry = entries.get(index);
|
||||
DataKey key = entry.getKey();
|
||||
try {
|
||||
if (key.type() == NativeBlockState.class
|
||||
&& writer.restorePrerequisiteCell(key.x(), key.y(), key.z())) {
|
||||
continue;
|
||||
}
|
||||
if (writer.restorePrerequisiteData(key.x(), key.y(), key.z(), key.type())) {
|
||||
continue;
|
||||
}
|
||||
writer.clearData(key.x(), key.y(), key.z(), key.type());
|
||||
Object original = entry.getValue();
|
||||
if (original != CLEARED) {
|
||||
writer.setData(key.x(), key.y(), key.z(), original);
|
||||
}
|
||||
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
|
||||
entry.getValue() == CLEARED ? null : entry.getValue());
|
||||
} catch (Throwable rollbackFailure) {
|
||||
if (rollbackFailure != failure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OverlayCell {
|
||||
private Object block;
|
||||
private Object hydrology;
|
||||
private Object cavern;
|
||||
private Object identifier;
|
||||
private Object tile;
|
||||
private Object string;
|
||||
private Object treeMaterial;
|
||||
private Map<Class<?>, Object> other;
|
||||
@@ -430,6 +429,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
if (type == Identifier.class) {
|
||||
return identifier;
|
||||
}
|
||||
if (type == TileWrapper.class) {
|
||||
return tile;
|
||||
}
|
||||
if (type == String.class) {
|
||||
return string;
|
||||
}
|
||||
@@ -446,6 +448,8 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
cavern = value;
|
||||
} else if (type == Identifier.class) {
|
||||
identifier = value;
|
||||
} else if (type == TileWrapper.class) {
|
||||
tile = value;
|
||||
} else if (type == String.class) {
|
||||
string = value;
|
||||
} else if (type == TreeBlockMaterial.class) {
|
||||
|
||||
+1
-3
@@ -19,9 +19,7 @@ final class StructureFoundationPlanner {
|
||||
return;
|
||||
}
|
||||
long columnKey = pack(x, z);
|
||||
if (!columns.containsKey(columnKey) || y < columns.get(columnKey)) {
|
||||
columns.put(columnKey, y);
|
||||
}
|
||||
columns.mergeInt(columnKey, y, Math::min);
|
||||
}
|
||||
|
||||
static int findGroundY(int foundationY, int maxDepth, int minimumY, IntPredicate solidAtY) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import art.arcane.iris.structure.placement.PlacedStructurePiece;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.structure.StructureCarvingFootprint;
|
||||
@@ -36,11 +38,12 @@ final class StructurePieceColumns {
|
||||
if (object == null || rotation == null || object.getBlocks() == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBlockVector local : object.getBlocks().keys()) {
|
||||
if (local == null || B.isAir(object.getBlocks().get(local))) {
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.getBlocks().cursor();
|
||||
while (cursor.next()) {
|
||||
if (B.isAir(cursor.value())) {
|
||||
continue;
|
||||
}
|
||||
IrisBlockVector rotated = rotation.rotate(local.clone());
|
||||
IrisBlockVector rotated = rotation.rotate(cursor.key());
|
||||
long worldX = (long) piece.getX() + rotated.getBlockX();
|
||||
long worldY = (long) piece.getY() + rotated.getBlockY();
|
||||
long worldZ = (long) piece.getZ() + rotated.getBlockZ();
|
||||
|
||||
@@ -22,13 +22,15 @@ public final class TerrainMatterView {
|
||||
}
|
||||
|
||||
public static <T> T get(Mantle<Matter> mantle, int x, int y, int z, Class<T> type) {
|
||||
if (!hasJournal(type)) {
|
||||
return mantle.get(x, y, z, type);
|
||||
Objects.requireNonNull(type, "Terrain matter type");
|
||||
if (y < 0 || y >= mantle.getWorldHeight()) {
|
||||
return null;
|
||||
}
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(x >> 4, z >> 4);
|
||||
synchronized (chunk) {
|
||||
PreObjectMatterCell cell = mantle.get(x, y, z, PreObjectMatterCell.class);
|
||||
return cell != null && cell.captures(type) ? cell.original(type) : mantle.get(x, y, z, type);
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(x >> 4, z >> 4);
|
||||
try {
|
||||
return get(chunk, x, y, z, type);
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ import lombok.experimental.Accessors;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisExpression extends IrisRegistrant {
|
||||
private static final int MAX_RETAINED_EVALUATION_DEPTH = 8;
|
||||
private static final int MAX_RETAINED_ARGUMENTS = 256;
|
||||
|
||||
@ArrayType(type = IrisExpressionLoad.class, min = 1)
|
||||
@Description("Variables to use in this expression")
|
||||
private KList<IrisExpressionLoad> variables = new KList<>();
|
||||
@@ -66,6 +69,10 @@ public class IrisExpression extends IrisRegistrant {
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient LazyBoundedCache<Long, ProceduralStream<Double>> streams = new LazyBoundedCache<>(8);
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient ThreadLocal<EvaluationScratch> evaluationScratch = ThreadLocal.withInitial(EvaluationScratch::new);
|
||||
|
||||
private Expression expression() {
|
||||
return expressionCache.aquire(() -> {
|
||||
Scope scope = new Scope(); // Create variable scope. This scope can hold both constants and invocation variables.
|
||||
@@ -108,31 +115,37 @@ public class IrisExpression extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public double evaluate(RNG rng, double x, double z) {
|
||||
double[] g = new double[3 + getVariables().size()];
|
||||
int m = 0;
|
||||
for (IrisExpressionLoad i : getVariables()) {
|
||||
g[m++] = i.getValue(rng, getLoader(), x, z);
|
||||
EvaluationScratch scratch = evaluationScratch.get();
|
||||
double[] arguments = scratch.acquire(3 + getVariables().size());
|
||||
try {
|
||||
int index = 0;
|
||||
for (IrisExpressionLoad variable : getVariables()) {
|
||||
arguments[index++] = variable.getValue(rng, getLoader(), x, z);
|
||||
}
|
||||
arguments[index++] = x;
|
||||
arguments[index++] = z;
|
||||
arguments[index] = -1;
|
||||
return expression().evaluate(new FunctionContext(rng), arguments);
|
||||
} finally {
|
||||
scratch.release();
|
||||
}
|
||||
|
||||
g[m++] = x;
|
||||
g[m++] = z;
|
||||
g[m] = -1;
|
||||
|
||||
return expression().evaluate(new FunctionContext(rng), g);
|
||||
}
|
||||
|
||||
public double evaluate(RNG rng, double x, double y, double z) {
|
||||
double[] g = new double[3 + getVariables().size()];
|
||||
int m = 0;
|
||||
for (IrisExpressionLoad i : getVariables()) {
|
||||
g[m++] = i.getValue(rng, getLoader(), x, y, z);
|
||||
EvaluationScratch scratch = evaluationScratch.get();
|
||||
double[] arguments = scratch.acquire(3 + getVariables().size());
|
||||
try {
|
||||
int index = 0;
|
||||
for (IrisExpressionLoad variable : getVariables()) {
|
||||
arguments[index++] = variable.getValue(rng, getLoader(), x, y, z);
|
||||
}
|
||||
arguments[index++] = x;
|
||||
arguments[index++] = y;
|
||||
arguments[index] = z;
|
||||
return expression().evaluate(new FunctionContext(rng), arguments);
|
||||
} finally {
|
||||
scratch.release();
|
||||
}
|
||||
|
||||
g[m++] = x;
|
||||
g[m++] = y;
|
||||
g[m] = z;
|
||||
|
||||
return expression().evaluate(new FunctionContext(rng), g);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,4 +157,28 @@ public class IrisExpression extends IrisRegistrant {
|
||||
public String getTypeName() {
|
||||
return "Expression";
|
||||
}
|
||||
|
||||
private static final class EvaluationScratch {
|
||||
private final double[][] retained = new double[MAX_RETAINED_EVALUATION_DEPTH][];
|
||||
private int depth;
|
||||
|
||||
private double[] acquire(int length) {
|
||||
double[] arguments;
|
||||
if (depth >= retained.length || length > MAX_RETAINED_ARGUMENTS) {
|
||||
arguments = new double[length];
|
||||
} else {
|
||||
arguments = retained[depth];
|
||||
if (arguments == null || arguments.length != length) {
|
||||
arguments = new double[length];
|
||||
retained[depth] = arguments;
|
||||
}
|
||||
}
|
||||
depth++;
|
||||
return arguments;
|
||||
}
|
||||
|
||||
private void release() {
|
||||
depth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ public class IrisGeneratorStyle {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isFlat() {
|
||||
return style == null || style.equals(NoiseStyle.FLAT);
|
||||
return expression == null && imageMap == null && (style == null || style.equals(NoiseStyle.FLAT));
|
||||
}
|
||||
|
||||
public double getMaxFractureDistance() {
|
||||
|
||||
@@ -210,8 +210,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += biome.getGenLinkMax(key, engine);
|
||||
minimum += biome.getGenLinkMin(key, engine);
|
||||
maximum += biome.getGenLinkMax(key);
|
||||
minimum += biome.getGenLinkMin(key);
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(minimum, maximum));
|
||||
}
|
||||
@@ -329,8 +329,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += sampledBiome.getGenLinkMax(key, engine);
|
||||
minimum += sampledBiome.getGenLinkMin(key, engine);
|
||||
maximum += sampledBiome.getGenLinkMax(key);
|
||||
minimum += sampledBiome.getGenLinkMin(key);
|
||||
}
|
||||
return new NoiseBounds(minimum, maximum);
|
||||
} catch (Throwable e) {
|
||||
|
||||
@@ -32,7 +32,14 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public final class GenerationCacheWarmer {
|
||||
private GenerationCacheWarmer() {
|
||||
@@ -43,6 +50,7 @@ public final class GenerationCacheWarmer {
|
||||
IrisData data = engine.getData();
|
||||
RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L);
|
||||
int[] counter = {0};
|
||||
List<ProceduralWarmTask> proceduralTasks = new ArrayList<>();
|
||||
|
||||
KList<IrisBiome> biomes = engine.getAllBiomes();
|
||||
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
||||
@@ -50,7 +58,7 @@ public final class GenerationCacheWarmer {
|
||||
warmPlacements(biome.getObjects(), root, counter, data, engine);
|
||||
warmDecorators(biome.getDecorators(), engine.getSeedManager().getComponent(), counter, data);
|
||||
warmOres(biome.getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
||||
collectProcedural(biome.getProceduralObjects(), root, counter, proceduralTasks);
|
||||
}
|
||||
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
@@ -58,10 +66,11 @@ public final class GenerationCacheWarmer {
|
||||
for (IrisRegion region : regions) {
|
||||
warmPlacements(region.getObjects(), root, counter, data, engine);
|
||||
warmOres(region.getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
||||
collectProcedural(region.getProceduralObjects(), root, counter, proceduralTasks);
|
||||
}
|
||||
|
||||
warmOres(engine.getDimension().getOres(), engine.getSeedManager().getTerrain(), counter, data);
|
||||
warmProcedural(proceduralTasks, data);
|
||||
|
||||
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
|
||||
}
|
||||
@@ -110,15 +119,93 @@ public final class GenerationCacheWarmer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void warmProcedural(IrisProceduralObjects procedural, RNG root, int[] counter, IrisData data) {
|
||||
private static void collectProcedural(IrisProceduralObjects procedural, RNG root, int[] counter,
|
||||
List<ProceduralWarmTask> tasks) {
|
||||
if (procedural == null) {
|
||||
return;
|
||||
}
|
||||
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
|
||||
if (placement == null) {
|
||||
continue;
|
||||
}
|
||||
placement.getVariantObject(data, root.nextParallelRNG(counter[0]++), null);
|
||||
if (placement != null) {
|
||||
tasks.add(new ProceduralWarmTask(placement, root.nextParallelRNG(counter[0]++)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void warmProcedural(List<ProceduralWarmTask> tasks, IrisData data) {
|
||||
if (tasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IdentityHashMap<IrisProceduralPlacement, List<ProceduralWarmTask>> identities = new IdentityHashMap<>();
|
||||
List<List<ProceduralWarmTask>> groups = new ArrayList<>();
|
||||
for (ProceduralWarmTask task : tasks) {
|
||||
List<ProceduralWarmTask> group = identities.get(task.placement());
|
||||
if (group == null) {
|
||||
group = new ArrayList<>();
|
||||
identities.put(task.placement(), group);
|
||||
groups.add(group);
|
||||
}
|
||||
group.add(task);
|
||||
}
|
||||
int workers = Math.min(groups.size(), Math.min(4, Runtime.getRuntime().availableProcessors()));
|
||||
if (workers == 1) {
|
||||
for (ProceduralWarmTask task : tasks) {
|
||||
task.placement().getVariantObject(data, task.rng(), null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Throwable failure = null;
|
||||
boolean interrupted = false;
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(workers, runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris-Procedural-Warm");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
})) {
|
||||
List<Future<?>> pending = new ArrayList<>(groups.size());
|
||||
for (List<ProceduralWarmTask> group : groups) {
|
||||
pending.add(executor.submit(() -> warmGroup(group, data)));
|
||||
}
|
||||
for (Future<?> future : pending) {
|
||||
boolean complete = false;
|
||||
while (!complete) {
|
||||
try {
|
||||
future.get();
|
||||
complete = true;
|
||||
} catch (InterruptedException exception) {
|
||||
interrupted = true;
|
||||
} catch (ExecutionException exception) {
|
||||
Throwable cause = exception.getCause();
|
||||
if (failure == null) {
|
||||
failure = cause;
|
||||
} else if (failure != cause) {
|
||||
failure.addSuppressed(cause);
|
||||
}
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IllegalStateException("Procedural generation cache warming failed.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void warmGroup(List<ProceduralWarmTask> group, IrisData data) {
|
||||
for (ProceduralWarmTask task : group) {
|
||||
task.placement().getVariantObject(data, task.rng(), null);
|
||||
}
|
||||
}
|
||||
|
||||
record ProceduralWarmTask(IrisProceduralPlacement placement, RNG rng) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1955,8 +1955,8 @@ public class IrisComplex implements DataProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
max += biome.getGenLinkMax(key, engine);
|
||||
min += biome.getGenLinkMin(key, engine);
|
||||
max += biome.getGenLinkMax(key);
|
||||
min += biome.getGenLinkMin(key);
|
||||
}
|
||||
|
||||
return new GeneratorBounds(min, max);
|
||||
|
||||
@@ -18,13 +18,9 @@
|
||||
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
|
||||
public interface MeteredCache {
|
||||
long getSize();
|
||||
|
||||
KCache<?, ?> getRawCache();
|
||||
|
||||
long getMaxSize();
|
||||
|
||||
default double getUsage() {
|
||||
|
||||
@@ -20,7 +20,6 @@ package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
@@ -33,7 +32,6 @@ import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class PreservationSVC implements IrisService, PreservationRegistry {
|
||||
@@ -188,10 +186,6 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
|
||||
caches.add(new WeakReference<>(cache));
|
||||
}
|
||||
|
||||
public List<KCache<?, ?>> caches() {
|
||||
return cacheStream().map(MeteredCache::getRawCache).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
public List<MeteredCache> getCaches() {
|
||||
return cacheStream().toList();
|
||||
|
||||
+24
-2
@@ -192,7 +192,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
IrisData data = terrainContext.getData();
|
||||
IrisBiome biome = layer.biome();
|
||||
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
|
||||
int bedrockDepth = dimension.isBedrock() ? 1 : 0;
|
||||
int surfaceDepth = Math.max(0, layer.normalTerrainHeight() + 1 - bedrockDepth);
|
||||
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
|
||||
KList<NativeBlockState> surfaceBlocks = biome == null || layer.terrainColumn() != null
|
||||
? null
|
||||
@@ -207,6 +208,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
|
||||
terrainContext.getSlopeStream()
|
||||
);
|
||||
int paletteSurfaceY = layer.normalTerrainHeight();
|
||||
KList<NativeBlockState> ceilingBlocks = null;
|
||||
int terrainSpan = layer.terrainColumn() == null ? -1 : layer.terrainColumn().spanCount() - 1;
|
||||
KList<NativeBlockState> seaBlocks = biome == null || fluidDepth == 0
|
||||
? null
|
||||
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
|
||||
@@ -239,13 +242,32 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
|
||||
if (sourceSurfaceY != paletteSurfaceY) {
|
||||
paletteSurfaceY = sourceSurfaceY;
|
||||
surfaceBlocks = null;
|
||||
ceilingBlocks = null;
|
||||
}
|
||||
if (depth == 0 && layer.surfaceBlock() != null) {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
|
||||
} else {
|
||||
if (terrainSpan >= 0) {
|
||||
while (terrainSpan >= 0 && sourceY < layer.terrainColumn().ceiling(terrainSpan)) {
|
||||
terrainSpan--;
|
||||
}
|
||||
int ceilingY = layer.terrainColumn().ceiling(terrainSpan);
|
||||
int ceilingDepth = sourceY - ceilingY;
|
||||
if (terrainSpan > 0 && depth >= 2 && ceilingDepth < 2) {
|
||||
if (ceilingBlocks == null && biome != null) {
|
||||
ceilingBlocks = biome.generateCeilingLayers(dimension, worldX, worldZ, rng,
|
||||
2, ceilingY, data, null);
|
||||
}
|
||||
writeBlock(output, metadata, localX, y, localZ,
|
||||
ceilingBlocks != null && ceilingBlocks.hasIndex(ceilingDepth)
|
||||
? ceilingBlocks.get(ceilingDepth) : layer.rockBlock());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (surfaceBlocks == null && biome != null) {
|
||||
surfaceBlocks = biome.generateLayersWithSlope(dimension, worldX, worldZ, rng,
|
||||
sourceSurfaceY, sourceSurfaceY, data, terrainContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
sourceSurfaceY + 1 - bedrockDepth, sourceSurfaceY, data,
|
||||
terrainContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
writeBlock(output, metadata, localX, y, localZ,
|
||||
surfaceBlocks != null && surfaceBlocks.hasIndex(depth)
|
||||
|
||||
+20
-7
@@ -178,15 +178,24 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
return block == null ? fallbackSolid : block;
|
||||
}
|
||||
|
||||
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, int paletteDepth, IrisData data, IrisComplex complex) {
|
||||
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, int paletteDepth, int surfaceY, int baseY, IrisData data, IrisComplex complex) {
|
||||
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
|
||||
RNG layerRng = paletteRng(rng, entry);
|
||||
KList<NativeBlockState> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
|
||||
boolean physicalDepth = usesPhysicalDepth(target, entry);
|
||||
int topPaletteDepth = physicalDepth ? Math.max(paletteDepth, surfaceY - baseY + 1) : paletteDepth;
|
||||
KList<NativeBlockState> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, topPaletteDepth, surfaceY, data, complex);
|
||||
if (topBlocks == null || topBlocks.isEmpty()) {
|
||||
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
|
||||
physicalDepth = usesPhysicalDepth(parent, entry);
|
||||
topPaletteDepth = physicalDepth ? Math.max(paletteDepth, surfaceY - baseY + 1) : paletteDepth;
|
||||
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, topPaletteDepth, surfaceY, data, complex);
|
||||
}
|
||||
KList<NativeBlockState> bottomBlocks = generateBottomPaletteLayers(entry, dimension, wx, wz, layerRng, paletteDepth, data, complex);
|
||||
return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone"));
|
||||
return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone"), surfaceY, physicalDepth);
|
||||
}
|
||||
|
||||
private static boolean usesPhysicalDepth(IrisBiome biome, IrisFloatingChildBiomes entry) {
|
||||
return biome != null && biome.isLockLayers() && (entry == null || entry.getBottomPaletteMode() == null
|
||||
|| entry.getBottomPaletteMode() == FloatingBottomPaletteMode.DEPTH);
|
||||
}
|
||||
|
||||
static RNG paletteRng(RNG worldRng, IrisFloatingChildBiomes entry) {
|
||||
@@ -198,11 +207,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
private final KList<NativeBlockState> topBlocks;
|
||||
private final KList<NativeBlockState> bottomBlocks;
|
||||
private final NativeBlockState fallbackSolid;
|
||||
private final int surfaceY;
|
||||
private final boolean physicalDepth;
|
||||
|
||||
private PaletteContext(KList<NativeBlockState> topBlocks, KList<NativeBlockState> bottomBlocks, NativeBlockState fallbackSolid) {
|
||||
private PaletteContext(KList<NativeBlockState> topBlocks, KList<NativeBlockState> bottomBlocks, NativeBlockState fallbackSolid, int surfaceY, boolean physicalDepth) {
|
||||
this.topBlocks = topBlocks;
|
||||
this.bottomBlocks = bottomBlocks;
|
||||
this.fallbackSolid = fallbackSolid;
|
||||
this.surfaceY = surfaceY;
|
||||
this.physicalDepth = physicalDepth;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,12 +264,12 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
|
||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||
PaletteContext paletteContext = paletteContexts.get(entry);
|
||||
if (paletteContext == null) {
|
||||
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, paletteDepth, data, complex);
|
||||
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, paletteDepth, y, sample.islandBaseY, data, complex);
|
||||
paletteContexts.put(entry, paletteContext);
|
||||
}
|
||||
int depth = topDepthByEntry.getOrDefault(entry, 0);
|
||||
int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k];
|
||||
NativeBlockState block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, depth, bottomDepth, paletteContext.fallbackSolid);
|
||||
NativeBlockState block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, paletteContext.physicalDepth ? paletteContext.surfaceY - y : depth, bottomDepth, paletteContext.fallbackSolid);
|
||||
if (block != null) {
|
||||
output.set(xf, y, zf, block);
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
cut = Math.max(0, naturalSurfaceY - layerSurfaceY);
|
||||
}
|
||||
blocks = biome.generateLayers(dimension, realX, realZ, rng,
|
||||
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
|
||||
layerSurfaceY + cut + 1 - bedrockFloor, layerSurfaceY + cut, data, complex);
|
||||
}
|
||||
|
||||
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
|
||||
@@ -436,7 +436,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
int depthFromFace = y - faceY;
|
||||
if (upperBlocks == null && upperBiome != null) {
|
||||
upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(),
|
||||
realX, realZ, rng, sourceSurfaceY, sourceSurfaceY,
|
||||
realX, realZ, rng, sourceSurfaceY + 1 - (bedrockEnabled ? 1 : 0), sourceSurfaceY,
|
||||
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
|
||||
}
|
||||
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.cache.WorldCache2DDouble;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
|
||||
public class CachedDoubleStream2D extends BasicStream<Double> implements ProceduralStream<Double>, MeteredCache, ChunkFillableStream2D, ChunkFillableDoubleStream2D {
|
||||
private final ProceduralStream<Double> stream;
|
||||
@@ -56,11 +55,6 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
|
||||
return cache.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KCache<?, ?> getRawCache() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxSize() {
|
||||
return cache.getMaxSize();
|
||||
|
||||
@@ -24,7 +24,6 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.volmlib.util.cache.ChunkCache2D;
|
||||
import art.arcane.volmlib.util.cache.WorldCache2D;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStream<T>, MeteredCache, ChunkFillableStream2D {
|
||||
@@ -70,11 +69,6 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
|
||||
return cache.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KCache<?, ?> getRawCache() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxSize() {
|
||||
return cache.getMaxSize();
|
||||
|
||||
@@ -64,11 +64,6 @@ public class CachedStream3D<T> extends BasicStream<T> implements ProceduralStrea
|
||||
return cache.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KCache<?, ?> getRawCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxSize() {
|
||||
return cache.getMaxSize();
|
||||
|
||||
@@ -6,17 +6,15 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
import art.arcane.volmlib.util.stream.BasicStream;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.Objects;
|
||||
public class ContextInjectingStream<T> extends BasicStream<T> {
|
||||
private final Engine engine;
|
||||
private final Function3<ChunkContext, Integer, Integer, T> contextAccessor;
|
||||
|
||||
public ContextInjectingStream(ProceduralStream<T> stream, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
|
||||
this(stream, null, contextAccessor);
|
||||
}
|
||||
|
||||
public ContextInjectingStream(ProceduralStream<T> stream, Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
|
||||
super(stream);
|
||||
this.engine = engine;
|
||||
this.engine = Objects.requireNonNull(engine);
|
||||
this.contextAccessor = contextAccessor;
|
||||
}
|
||||
|
||||
@@ -30,9 +28,9 @@ public class ContextInjectingStream<T> extends BasicStream<T> {
|
||||
int blockZ = (int) z;
|
||||
|
||||
if (chunkContext != null
|
||||
&& (engine == null || context.getEngine() == engine)
|
||||
&& context.getEngine() == engine
|
||||
&& context.getGenerationSessionId() == chunkContext.getGenerationSessionId()
|
||||
&& (engine == null || context.getGenerationSessionId() == engine.getGenerationSessionId())
|
||||
&& context.getGenerationSessionId() == engine.getGenerationSessionId()
|
||||
&& blockX >> 4 == chunkContext.getX() >> 4
|
||||
&& blockZ >> 4 == chunkContext.getZ() >> 4) {
|
||||
T t = contextAccessor.apply(chunkContext, blockX & 15, blockZ & 15);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package art.arcane.iris.generation.stream;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.function.Function3;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
|
||||
public final class GenerationStreams {
|
||||
private GenerationStreams() {
|
||||
@@ -25,19 +21,7 @@ public final class GenerationStreams {
|
||||
return new CachedStream3D<T>(name, engine, stream, maxSize);
|
||||
}
|
||||
|
||||
public static <T> ProceduralStream<T> contextInjecting(ProceduralStream<T> stream, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
|
||||
//return stream;
|
||||
return new ContextInjectingStream<>(stream, contextAccessor);
|
||||
}
|
||||
|
||||
public static <T> ProceduralStream<T> contextInjecting(ProceduralStream<T> stream, Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
|
||||
return new ContextInjectingStream<>(stream, engine, contextAccessor);
|
||||
}
|
||||
|
||||
public static <T> ProceduralStream<Double> style(ProceduralStream<T> stream, RNG rng, IrisStyledRange range, IrisData data) {
|
||||
return ProceduralStream.of((x, z) -> {
|
||||
double d = stream.getDouble(x, z);
|
||||
return range.get(rng, d, -d, data);
|
||||
}, Interpolated.DOUBLE);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user