Refine terrain and pregeneration

- Add coherent river width variation and shape source and terminal pools.
- Preserve separated hydrology intervals when checking river connectivity.
- Reduce footprint sorting, engine context lookup, and terrain blending work.
- Reuse modded snapshot validation after content and registry checks.
- Explain production pack update staging and restart requirements.
- Retain pregeneration map updates before its desktop window opens.
- Improve pregeneration layout, coordinate hover, keyboard controls, and error details.
- Add the new desktop labels to every supported language catalog.
- Add the buildPsychoLT entry point for verified platform outputs.
- Cover terrain, validation, and desktop behavior with regression tests.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-07 10:25:41 -04:00
1 parent e72189adc7
commit cfa274825f
56 files changed
+1656 -280

No files matched your search

@@ -6,10 +6,9 @@ import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.pack.BrokenPackException;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.world.history.GenerationEpoch;
import art.arcane.iris.world.history.GenerationEpochContractFactory;
import art.arcane.iris.world.history.GenerationHistory;
@@ -174,7 +173,7 @@ final class ModdedGenerationHistoryStorage {
static ActivePack resolveActive(GenerationHistory history) throws IOException {
GenerationEpoch epoch = history.activeEpoch();
Path packRoot = normalize(history.activePackRoot());
validatePack(packRoot, epoch.packFingerprint());
validatePack(packRoot);
validateDimension(packRoot, epoch);
return new ActivePack(
packRoot,
@@ -184,13 +183,16 @@ final class ModdedGenerationHistoryStorage {
);
}
private static void validatePack(Path packRoot, String fingerprint) {
String contextFingerprint = PackValidationCache.contextFingerprint();
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
PackValidationRegistry.publish(packRoot, validation, fingerprint, contextFingerprint);
static PackValidationResult validatePack(Path packRoot) {
PackValidationResult validation = PackValidationRegistry.getMatching(
packRoot, PackFingerprints.computePackTreeFingerprint(packRoot.toFile()));
if (validation == null) {
validation = ModdedStartup.validatePack(packRoot.toFile());
}
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
}
return validation;
}
private static void validateDimension(
@@ -119,7 +119,8 @@ public final class ModdedPackUpdateService implements ModdedTickableService {
+ ", 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.");
updates.add("Existing worlds retain their recorded production pack. Back up the complete world, then stage an update with /iris world update <dimension> "
+ pack.name() + " and restart the server to activate it.");
}
}
notices = List.copyOf(updates);
@@ -3,6 +3,7 @@ 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.PackValidationCache;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import org.junit.After;
@@ -15,6 +16,7 @@ import java.io.File;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -22,6 +24,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
public class ModdedPackValidationPublicationTest {
@Rule
@@ -32,6 +35,55 @@ public class ModdedPackValidationPublicationTest {
PackValidationRegistry.clear();
}
@Test
public void repeatedSnapshotValidationParsesUnchangedPackOnce() throws Exception {
File root = temporaryFolder.newFolder("snapshot");
PackValidationResult result = loadable(root);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(result);
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
assertSame(result, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
validator.verify(() -> PackValidator.validate(root), times(1));
}
}
@Test
public void snapshotValidationRejectsChangedBytesWithPreservedTimestamp() throws Exception {
File root = temporaryFolder.newFolder("snapshot");
File content = new File(root, "content.json");
Files.writeString(content.toPath(), "valid");
FileTime timestamp = Files.getLastModifiedTime(content.toPath());
PackValidationResult broken = new PackValidationResult(root.getName(), List.of("invalid"), List.of(), 1L);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class)) {
validator.when(() -> PackValidator.validate(root)).thenReturn(loadable(root), broken);
ModdedGenerationHistoryStorage.validatePack(root.toPath());
Files.writeString(content.toPath(), "wrong");
Files.setLastModifiedTime(content.toPath(), timestamp);
assertThrows(BrokenPackException.class,
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
assertThrows(BrokenPackException.class,
() -> ModdedGenerationHistoryStorage.validatePack(root.toPath()));
validator.verify(() -> PackValidator.validate(root), times(2));
}
}
@Test
public void snapshotValidationReparsesChangedRegistryContext() throws Exception {
File root = temporaryFolder.newFolder("snapshot");
AtomicReference<String> context = new AtomicReference<>("first");
PackValidationResult first = loadable(root);
PackValidationResult second = loadable(root);
try (MockedStatic<PackValidator> validator = mockStatic(PackValidator.class);
MockedStatic<PackValidationCache> cache = mockStatic(PackValidationCache.class)) {
cache.when(PackValidationCache::contextFingerprint).thenAnswer(ignored -> context.get());
validator.when(() -> PackValidator.validate(root)).thenReturn(first, second);
assertSame(first, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
context.set("second");
assertSame(second, ModdedGenerationHistoryStorage.validatePack(root.toPath()));
validator.verify(() -> PackValidator.validate(root), times(2));
}
}
@Test
public void validationPublishesTheCanonicalRootAndPackName() throws Exception {
File root = temporaryFolder.newFolder("terrain");
@@ -65,6 +65,9 @@ public class ModdedPackUpdateServiceTest {
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, times(1)).sendMessage(contains("Back up the complete world"));
verify(online, times(1)).sendMessage(contains("/iris world update <dimension> overworld"));
verify(online, times(1)).sendMessage(contains("restart the server to activate it"));
verify(online, never()).sendMessage(contains("plugins/"));
NativeProtocolPlayer later = player(true, false);
service.notifyPlayer(later);