mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
This commit is contained in:
+141
-78
@@ -100,13 +100,17 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceBeardAdjustmentsPrepareTerrainAcrossDeclaredSteps() {
|
||||
public void sourceBeardAdjustmentsPrepareOnlyAtTheSurfaceStructuresStep() {
|
||||
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.FLUID_SPRINGS));
|
||||
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
|
||||
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.STRONGHOLDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,6 +123,31 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
TerrainAdjustment.NONE, GenerationStep.Decoration.FLUID_SPRINGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shallowUndergroundBeardBoxDoesNotMutateTheTopSurface() throws Exception {
|
||||
PoolElementStructurePiece piece = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template(List.of(
|
||||
block(0, 1, 0, Blocks.DEEPSLATE_BRICKS.defaultBlockState())))),
|
||||
new BoundingBox(0, 67, 0, 0, 74, 0), 1, Rotation.NONE);
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(),
|
||||
GenerationStep.Decoration.UNDERGROUND_DECORATION,
|
||||
TerrainAdjustment.BEARD_BOX));
|
||||
StructureStart start = new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of(piece)));
|
||||
BoundingBox area = new BoundingBox(0, 48, 0, 0, 80, 0);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 64);
|
||||
Map<BlockPos, BlockState> before = new HashMap<>(blocks);
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, List.of(surfaceTarget(start)),
|
||||
(x, z) -> 64);
|
||||
|
||||
assertEquals(before, blocks);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitTerrainOverrideDisablesSourceBeardFitting() {
|
||||
StructureStart start = desertStart(TerrainAdjustment.BEARD_BOX);
|
||||
@@ -158,85 +187,70 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void beardBoxUsesTheFullRigidHeightWhileBeardThinUsesTheGroundPlane() {
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor thin =
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 64, 2);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor box =
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(
|
||||
0, 4, 0, 4, 64, 2, 65, 90);
|
||||
|
||||
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(thin), 2, 2, 80));
|
||||
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(box), 2, 2, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void beardInfluenceBeginsAtGroundYRatherThanTheSurfaceBlock() {
|
||||
public void sourceSurfaceAnchorMeetsOneBlockBelowTheAuthoredGroundPlane() {
|
||||
BoundingBox bounds = new BoundingBox(0, 64, 0, 4, 90, 4);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor thin =
|
||||
NativeStructureSurfaceFitter.surfaceAnchor(
|
||||
bounds, 68, 2, TerrainAdjustment.BEARD_THIN);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor box =
|
||||
NativeStructureSurfaceFitter.surfaceAnchor(
|
||||
bounds, 68, 2, TerrainAdjustment.BEARD_BOX);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor anchor =
|
||||
NativeStructureSurfaceFitter.surfaceAnchor(bounds, 68, 2);
|
||||
|
||||
assertEquals(67, thin.meetY());
|
||||
assertEquals(0, thin.verticalDistance(68));
|
||||
assertEquals(1, thin.verticalDistance(67));
|
||||
assertEquals(0, box.verticalDistance(68));
|
||||
assertEquals(1, box.verticalDistance(67));
|
||||
assertEquals(0, box.verticalDistance(90));
|
||||
assertEquals(1, box.verticalDistance(91));
|
||||
assertEquals(67, anchor.meetY());
|
||||
assertEquals(0, anchor.minX());
|
||||
assertEquals(4, anchor.maxX());
|
||||
assertEquals(0, anchor.minZ());
|
||||
assertEquals(4, anchor.maxZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rigidFootprintsRaiseTerrainAcrossGapsLargerThanTheBeardKernel() {
|
||||
public void rigidFootprintsBoundTerrainAcrossGapsLargerThanTheBeardKernel() {
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(72, 2);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(72, 1);
|
||||
|
||||
assertEquals(72, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(rigid), 2, 2, 40));
|
||||
assertEquals(40, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
assertEquals(46, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(junction), 2, 2, 40));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void elevatedRigidFootprintsBlendIntoAHorizontalRescueTaper() {
|
||||
int originalY = 40;
|
||||
int kernelEdge = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(51, 2)), 5, 2, originalY);
|
||||
int gapThirteen = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(52, 2)), 5, 2, originalY);
|
||||
int gapEighteen = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(57, 2)), 5, 2, originalY);
|
||||
int gapTwentyFour = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(63, 2)), 5, 2, originalY);
|
||||
public void beardSkirtBoundsAndGradesUpwardAndDownwardSymmetrically() {
|
||||
int originalY = 64;
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor high = anchor(96, 2);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor low = anchor(32, 2);
|
||||
int[] raised = new int[13];
|
||||
int[] lowered = new int[13];
|
||||
for (int outset = 0; outset <= 12; outset++) {
|
||||
int x = 4 + outset;
|
||||
raised[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(high), x, 2, originalY);
|
||||
lowered[outset] = NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(low), x, 2, originalY);
|
||||
}
|
||||
|
||||
assertEquals(originalY, kernelEdge);
|
||||
assertTrue(gapThirteen >= kernelEdge);
|
||||
assertTrue(gapEighteen > gapThirteen);
|
||||
assertTrue(gapTwentyFour > gapEighteen);
|
||||
assertTrue(gapTwentyFour < 63);
|
||||
assertEquals(originalY, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(63, 2)), 16, 2, originalY));
|
||||
assertEquals(originalY, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(63, 1)), 5, 2, originalY));
|
||||
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor(64, 2)), 5, 2, 80));
|
||||
assertArrayEquals(
|
||||
new int[]{70, 69, 68, 67, 67, 66, 66, 65, 65, 64, 64, 64, 64},
|
||||
raised);
|
||||
assertArrayEquals(
|
||||
new int[]{58, 59, 60, 61, 61, 62, 62, 63, 63, 64, 64, 64, 64},
|
||||
lowered);
|
||||
for (int outset = 0; outset <= 12; outset++) {
|
||||
assertEquals(raised[outset] - originalY, originalY - lowered[outset]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectedCenterAndRigidChildrenShareTerrainSupportAcrossTheAssembly() {
|
||||
public void projectedCenterAndRigidChildrenUseProcessedFoundationCells() throws Exception {
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(),
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES,
|
||||
TerrainAdjustment.BEARD_BOX));
|
||||
PoolElementStructurePiece center = rigidPiece(
|
||||
new BoundingBox(0, 71, 0, 3, 80, 3), 1);
|
||||
PoolElementStructurePiece child = rigidPiece(
|
||||
new BoundingBox(10, 65, 0, 13, 76, 3), 7);
|
||||
PoolElementStructurePiece center = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template(List.of(
|
||||
block(1, 1, 1, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 71, 0, 3, 80, 3), 1, Rotation.NONE);
|
||||
PoolElementStructurePiece child = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template(List.of(
|
||||
block(1, 7, 1, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(10, 65, 0, 13, 76, 3), 7, Rotation.NONE);
|
||||
StructureStart start = new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of(center, child)));
|
||||
@@ -244,8 +258,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
for (int x = area.minX(); x <= area.maxX(); x++) {
|
||||
for (int z = area.minZ(); z <= area.maxZ(); z++) {
|
||||
put(blocks, x, 63, z, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, 64, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
put(blocks, x, 64, z, Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, x, 65, z, Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,21 +269,66 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)));
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, targets,
|
||||
(x, z) -> 64);
|
||||
(x, z) -> 65);
|
||||
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 1, 71, 1));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 11, 71, 1));
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, 11, 70, 1));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 2, 65, 2));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 12, 65, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainMatchingFootprintsRaiseAndLowerOnlyLowestSolidColumns() throws Exception {
|
||||
public void sparseRigidFootprintsIgnoreAirAndSolidsAboveTheGroundPlane() throws Exception {
|
||||
StructureTemplate sparseTemplate = template(List.of(
|
||||
block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState()),
|
||||
block(15, 1, 0, Blocks.AIR.defaultBlockState()),
|
||||
block(30, 5, 0, Blocks.STONE.defaultBlockState())));
|
||||
PoolElementStructurePiece piece = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(sparseTemplate),
|
||||
new BoundingBox(0, 68, 0, 30, 76, 0), 1, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(
|
||||
List.of(piece), TerrainAdjustment.BEARD_THIN);
|
||||
BoundingBox area = new BoundingBox(0, 52, 0, 30, 80, 0);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 64);
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, List.of(surfaceTarget(start)),
|
||||
(x, z) -> 64);
|
||||
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 64, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extremeRigidFoundationGapDoesNotCreateATerrainPillar() throws Exception {
|
||||
StructureTemplate template = template(List.of(
|
||||
block(0, 1, 0, Blocks.COBBLESTONE.defaultBlockState())));
|
||||
PoolElementStructurePiece piece = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template),
|
||||
new BoundingBox(0, 90, 0, 0, 96, 0), 1, Rotation.NONE);
|
||||
StructureStart start = rigidSurfaceStart(
|
||||
List.of(piece), TerrainAdjustment.BEARD_THIN);
|
||||
BoundingBox area = new BoundingBox(0, 30, 0, 0, 100, 0);
|
||||
Map<BlockPos, BlockState> blocks = flatTerrain(area, 40);
|
||||
|
||||
NativeStructureSurfaceFitter.prepareSurfaceStructures(
|
||||
world(blocks), area, List.of(surfaceTarget(start)),
|
||||
(x, z) -> 40);
|
||||
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 46, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 47, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 0, 89, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terrainMatchingFootprintsFollowProcessedLowestSolidColumns() throws Exception {
|
||||
StructureTemplate pathTemplate = template(List.of(
|
||||
block(0, 0, 0, Blocks.AIR.defaultBlockState()),
|
||||
block(0, 1, 0, Blocks.DIRT_PATH.defaultBlockState()),
|
||||
block(0, 2, 0, Blocks.DIRT.defaultBlockState()),
|
||||
block(15, 0, 0, Blocks.AIR.defaultBlockState()),
|
||||
block(30, 0, 0, Blocks.AIR.defaultBlockState()),
|
||||
block(30, 1, 0, Blocks.DIRT_PATH.defaultBlockState()),
|
||||
block(30, 0, 0, Blocks.DIRT.defaultBlockState()),
|
||||
block(45, 0, 0, Blocks.DANDELION.defaultBlockState())));
|
||||
InlineSinglePoolElement element = new InlineSinglePoolElement(
|
||||
pathTemplate, List.of(), StructureTemplatePool.Projection.TERRAIN_MATCHING);
|
||||
@@ -290,8 +349,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
world(blocks), area, List.of(surfaceTarget(start)),
|
||||
(x, z) -> x == 0 ? 60 : x == 30 ? 72 : 64);
|
||||
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 68, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 68, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 0, 61, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 30, 71, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 30, 72, 0));
|
||||
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(blocks, 15, 64, 0));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(), state(blocks, 15, 65, 0));
|
||||
@@ -770,10 +829,10 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
|
||||
public void surfaceAnchorCapsInsideAndIsUnchangedAtRadius() {
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(72, 2);
|
||||
|
||||
assertEquals(72, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor), 2, 2, 64));
|
||||
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(anchor), 16, 2, 64));
|
||||
@@ -786,7 +845,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(68, 2);
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2);
|
||||
|
||||
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(raised), 10, 2, 64));
|
||||
assertEquals(67, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(lowered), 10, 2, 68));
|
||||
@@ -1197,16 +1256,20 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedRigidPiecesPreserveTheLowerAuthoredSurface() {
|
||||
public void stackedRigidPiecesPreserveTheLowerProcessedFoundation() throws Exception {
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(),
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES,
|
||||
TerrainAdjustment.BEARD_BOX));
|
||||
PoolElementStructurePiece lower = rigidPiece(
|
||||
new BoundingBox(0, 62, 0, 4, 65, 4), 1);
|
||||
PoolElementStructurePiece upper = rigidPiece(
|
||||
new BoundingBox(0, 66, 0, 4, 78, 4), 1);
|
||||
PoolElementStructurePiece lower = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template(List.of(
|
||||
block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 62, 0, 4, 65, 4), 1, Rotation.NONE);
|
||||
PoolElementStructurePiece upper = rigidTemplatePiece(
|
||||
new InlineSinglePoolElement(template(List.of(
|
||||
block(2, 1, 2, Blocks.COBBLESTONE.defaultBlockState())))),
|
||||
new BoundingBox(0, 66, 0, 4, 78, 4), 1, Rotation.NONE);
|
||||
StructureStart start = new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of(upper, lower)));
|
||||
@@ -1236,7 +1299,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
|
||||
NativeStructureSurfaceFitter.SurfaceAnchor adjacent =
|
||||
new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 70, 2);
|
||||
|
||||
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
assertEquals(69, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(adjacent), 4, 2, 64));
|
||||
assertEquals(66, NativeStructureSurfaceFitter.resolveSurfaceTarget(
|
||||
List.of(local, adjacent), 4, 2, 64));
|
||||
|
||||
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -669,7 +669,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
watch::readSettingsContent,
|
||||
watch::normalizeSettingsContent
|
||||
);
|
||||
configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of());
|
||||
configHotloadEngine.configure(500L, 3_000L, List.of(watch.settingsFile()), List.of());
|
||||
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
|
||||
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
||||
@@ -714,7 +714,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
pendingWorldReplacements.captureVanillaLevelContext();
|
||||
pendingWorldReplacements.verifyLoadedPublishedWorlds();
|
||||
J.a(this::bstats);
|
||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
|
||||
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 10);
|
||||
J.sr(this::tickQueue, 0);
|
||||
J.s(this::setupPapi);
|
||||
if (IrisStartupValidation.isReady()) {
|
||||
|
||||
+38
-10
@@ -21,9 +21,12 @@ package art.arcane.iris.core;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -31,6 +34,8 @@ import java.util.List;
|
||||
* {@link ConfigHotloadEngine} is built from and drains the touched-file queue.
|
||||
*/
|
||||
public final class SettingsHotloadWatch {
|
||||
private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
private final File settingsFile;
|
||||
|
||||
public SettingsHotloadWatch(File settingsFile) {
|
||||
@@ -46,13 +51,17 @@ public final class SettingsHotloadWatch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : engine.pollTouchedFiles()) {
|
||||
engine.processFileChange(file, ignored -> {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
IrisLanguage.reload();
|
||||
return true;
|
||||
}, ignored -> Iris.info("Hotloaded settings.json "));
|
||||
for (ConfigHotloadEngine.StableContentSnapshot snapshot : engine.pollTouchedSnapshots()) {
|
||||
if ("missing".equals(snapshot.signature())) {
|
||||
engine.processSnapshotChange(snapshot, ignored -> true, null);
|
||||
Iris.warn("settings.json was removed; retaining the last valid runtime settings.");
|
||||
continue;
|
||||
}
|
||||
engine.processSnapshotChange(
|
||||
snapshot,
|
||||
stable -> applySettingsSnapshot(stable.file(), stable.normalizedContent()),
|
||||
ignored -> Iris.info("Hotloaded settings.json ")
|
||||
);
|
||||
}
|
||||
IrisLanguage.update();
|
||||
}
|
||||
@@ -76,8 +85,12 @@ public final class SettingsHotloadWatch {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return IO.readAll(file);
|
||||
try (InputStream input = Files.newInputStream(file.toPath())) {
|
||||
byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1);
|
||||
if (content.length > MAX_SETTINGS_BYTES) {
|
||||
throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file);
|
||||
}
|
||||
return new String(content, StandardCharsets.UTF_8);
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read settings file %s: %s%s",
|
||||
file.getAbsolutePath(),
|
||||
@@ -95,4 +108,19 @@ public final class SettingsHotloadWatch {
|
||||
|
||||
return text.replace("\r\n", "\n").trim();
|
||||
}
|
||||
|
||||
private boolean applySettingsSnapshot(File file, String content) {
|
||||
if (content == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload);
|
||||
} catch (RuntimeException failure) {
|
||||
Iris.warn("Rejected invalid settings hotload from %s: %s",
|
||||
file.getAbsolutePath(),
|
||||
failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage());
|
||||
Iris.reportError(failure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
|
||||
@@ -960,6 +961,11 @@ public class CommandJigsaw implements DirectorExecutor {
|
||||
Throwable failure = unwrapCompletionFailure(throwable == null
|
||||
? new IllegalStateException("Studio open completed without a result")
|
||||
: throwable);
|
||||
if (isExpectedOpenDenial(failure)) {
|
||||
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
|
||||
structureKey, failure.getMessage());
|
||||
return;
|
||||
}
|
||||
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
|
||||
sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
|
||||
}
|
||||
@@ -1022,6 +1028,10 @@ public class CommandJigsaw implements DirectorExecutor {
|
||||
return current;
|
||||
}
|
||||
|
||||
static boolean isExpectedOpenDenial(Throwable failure) {
|
||||
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
|
||||
}
|
||||
|
||||
private void sendError(String message) {
|
||||
sender().sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class WandSVC implements IrisService {
|
||||
var latch = new CountDownLatch(1);
|
||||
var holder = Iris.tickets.getHolder(p.getWorld());
|
||||
new Job() {
|
||||
private int i;
|
||||
private volatile int i;
|
||||
private Chunk chunk;
|
||||
|
||||
@Override
|
||||
|
||||
+19
-1
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
|
||||
@@ -7,6 +8,7 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
|
||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec;
|
||||
import art.arcane.iris.core.service.JigsawStudioService;
|
||||
import art.arcane.iris.core.structure.authoring.StructureBackend;
|
||||
import art.arcane.iris.core.structure.authoring.StructureCapability;
|
||||
import art.arcane.iris.core.structure.authoring.StructureHash;
|
||||
@@ -17,7 +19,6 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
|
||||
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind;
|
||||
import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.core.tools.IrisCreator;
|
||||
import art.arcane.iris.core.service.JigsawStudioService;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDirection;
|
||||
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||
@@ -44,6 +45,7 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -101,6 +103,18 @@ public class CommandJigsawContractTest {
|
||||
CommandJigsaw.STUDIO_OPEN_KIND.datapackPreparation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packValidationDenialsAreExpectedOpenFailures() {
|
||||
BrokenPackException failure = new BrokenPackException(
|
||||
"overworld", List.of("Biome 'broken' has no resolvable regions."));
|
||||
|
||||
assertTrue(CommandJigsaw.isExpectedOpenDenial(failure));
|
||||
assertTrue(CommandJigsaw.isExpectedOpenDenial(
|
||||
new CompletionException(failure)));
|
||||
assertFalse(CommandJigsaw.isExpectedOpenDenial(
|
||||
new IllegalStateException("Unexpected Studio failure")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void committedActivationStartsInitialEvaluationBeforePlayerBinding() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
@@ -129,6 +143,10 @@ public class CommandJigsawContractTest {
|
||||
NamespacedKey source = CommandJigsaw.parseRegisteredStructureKey("minecraft:village_plains");
|
||||
assertEquals("minecraft_village_plains", CommandJigsaw.resolveConversionTarget(source, "auto"));
|
||||
assertEquals("villages/plains", CommandJigsaw.resolveConversionTarget(source, "iris:villages/plains"));
|
||||
NamespacedKey ancientCity = CommandJigsaw.parseRegisteredStructureKey(
|
||||
"minecraft:ancient_city");
|
||||
assertEquals("minecraft_ancient_city",
|
||||
CommandJigsaw.resolveConversionTarget(ancientCity, "auto"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> CommandJigsaw.resolveConversionTarget(source, "custom:village"));
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.1.1'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186'))
|
||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||
String coordinatePrefix = "${minecraftVersion}-"
|
||||
if (loaderVersion.startsWith(coordinatePrefix)) {
|
||||
|
||||
+59
-73
@@ -33,6 +33,7 @@ public final class NativeStructureSurfaceFitter {
|
||||
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
|
||||
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
|
||||
private static final int SURFACE_TERRAIN_RADIUS = 12;
|
||||
private static final int MAX_SOURCE_SURFACE_DELTA = 6;
|
||||
private static final int MAX_SURFACE_TEMPLATE_CELLS = 4_194_304;
|
||||
private static final Set<String> WARNED_SOURCE_BUDGET =
|
||||
ConcurrentHashMap.newKeySet();
|
||||
@@ -113,8 +114,9 @@ public final class NativeStructureSurfaceFitter {
|
||||
|
||||
static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment,
|
||||
GenerationStep.Decoration step) {
|
||||
return adjustment == TerrainAdjustment.BEARD_THIN
|
||||
|| adjustment == TerrainAdjustment.BEARD_BOX;
|
||||
return step == GenerationStep.Decoration.SURFACE_STRUCTURES
|
||||
&& (adjustment == TerrainAdjustment.BEARD_THIN
|
||||
|| adjustment == TerrainAdjustment.BEARD_BOX);
|
||||
}
|
||||
|
||||
static int surfaceTerrainRadius() {
|
||||
@@ -131,50 +133,27 @@ public final class NativeStructureSurfaceFitter {
|
||||
int localTargetY = originalY;
|
||||
SurfaceAnchor selectedLocal = null;
|
||||
long totalInfluence = 0L;
|
||||
long weightedMeetY = 0L;
|
||||
long weightedTargetY = 0L;
|
||||
long maximumInfluence = 0L;
|
||||
for (SurfaceAnchor anchor : anchors) {
|
||||
int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX());
|
||||
int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ());
|
||||
boolean containsColumn = outX == 0 && outZ == 0;
|
||||
if (containsColumn && anchor.strength() > 1 && originalY < anchor.meetY()) {
|
||||
if (precedes(anchor, selectedLocal)) {
|
||||
localTargetY = anchor.meetY();
|
||||
selectedLocal = anchor;
|
||||
}
|
||||
long distanceSquared = (long) outX * outX + (long) outZ * outZ;
|
||||
long radiusSquared = (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS;
|
||||
if (distanceSquared > radiusSquared) {
|
||||
continue;
|
||||
}
|
||||
int verticalDistance = anchor.verticalDistance(originalY);
|
||||
long horizontalDistanceSquared = (long) outX * outX + (long) outZ * outZ;
|
||||
long distanceSquared = (long) outX * outX + (long) outZ * outZ
|
||||
+ (long) verticalDistance * verticalDistance;
|
||||
double factor = 0D;
|
||||
long radiusSquared = (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS;
|
||||
if (distanceSquared <= radiusSquared) {
|
||||
double distance = Math.sqrt(distanceSquared);
|
||||
factor = Math.pow(
|
||||
double factor = Math.pow(
|
||||
1D - distance / SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF);
|
||||
}
|
||||
if (anchor.strength() > 1 && originalY < anchor.meetY()
|
||||
&& verticalDistance > SURFACE_TERRAIN_RADIUS
|
||||
&& horizontalDistanceSquared <= radiusSquared) {
|
||||
double horizontalDistance = Math.sqrt(horizontalDistanceSquared);
|
||||
double horizontalFactor = Math.pow(
|
||||
1D - horizontalDistance / SURFACE_TERRAIN_RADIUS,
|
||||
SURFACE_TERRAIN_FALLOFF);
|
||||
double rescueProgress = Math.min(1D,
|
||||
(verticalDistance - SURFACE_TERRAIN_RADIUS)
|
||||
/ (double) SURFACE_TERRAIN_RADIUS);
|
||||
double rescueWeight = rescueProgress * rescueProgress
|
||||
* (3D - 2D * rescueProgress);
|
||||
factor = Math.max(factor, horizontalFactor * rescueWeight);
|
||||
}
|
||||
if (factor <= 0D) {
|
||||
continue;
|
||||
}
|
||||
int boundedTargetY = boundedSurfaceTarget(originalY, anchor.meetY());
|
||||
if (containsColumn) {
|
||||
if (precedes(anchor, selectedLocal)) {
|
||||
localTargetY = anchor.meetY();
|
||||
localTargetY = boundedTargetY;
|
||||
selectedLocal = anchor;
|
||||
}
|
||||
continue;
|
||||
@@ -185,19 +164,35 @@ public final class NativeStructureSurfaceFitter {
|
||||
}
|
||||
long weightedInfluence = influence * Math.max(1, anchor.strength());
|
||||
totalInfluence += weightedInfluence;
|
||||
weightedMeetY += weightedInfluence * anchor.meetY();
|
||||
weightedTargetY += weightedInfluence * boundedTargetY;
|
||||
maximumInfluence = Math.max(maximumInfluence, influence);
|
||||
}
|
||||
if (selectedLocal != null) {
|
||||
return new SurfaceResolution(localTargetY, selectedLocal.strength() > 1);
|
||||
return new SurfaceResolution(
|
||||
localTargetY,
|
||||
selectedLocal.strength() > 1
|
||||
&& localTargetY == selectedLocal.meetY());
|
||||
}
|
||||
if (totalInfluence == 0L) {
|
||||
return new SurfaceResolution(originalY, false);
|
||||
}
|
||||
double blendedMeetY = weightedMeetY / (double) totalInfluence;
|
||||
double blendedTargetY = weightedTargetY / (double) totalInfluence;
|
||||
double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE;
|
||||
return new SurfaceResolution(
|
||||
(int) Math.round(originalY + ((blendedMeetY - originalY) * factor)), false);
|
||||
blendSurfaceTarget(originalY, blendedTargetY, factor), false);
|
||||
}
|
||||
|
||||
private static int boundedSurfaceTarget(int originalY, int meetY) {
|
||||
int delta = Math.max(
|
||||
-MAX_SOURCE_SURFACE_DELTA,
|
||||
Math.min(MAX_SOURCE_SURFACE_DELTA, meetY - originalY));
|
||||
return originalY + delta;
|
||||
}
|
||||
|
||||
private static int blendSurfaceTarget(int originalY, double targetY, double factor) {
|
||||
double delta = (targetY - originalY) * factor;
|
||||
int magnitude = (int) Math.round(Math.abs(delta));
|
||||
return originalY + (delta < 0D ? -magnitude : magnitude);
|
||||
}
|
||||
|
||||
private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) {
|
||||
@@ -235,12 +230,16 @@ public final class NativeStructureSurfaceFitter {
|
||||
area.maxX() + SURFACE_TERRAIN_RADIUS, area.maxY(),
|
||||
area.maxZ() + SURFACE_TERRAIN_RADIUS);
|
||||
List<SurfaceAnchor> anchors = new ArrayList<>();
|
||||
Set<StructureStart> seenStarts = Collections.newSetFromMap(
|
||||
new IdentityHashMap<>());
|
||||
for (NativeStructureTerrainIntegrator.TerrainTarget target : targets) {
|
||||
if (!requiresSourceSurfaceTerrain(target)) {
|
||||
continue;
|
||||
}
|
||||
StructureStart start = target.start();
|
||||
TerrainAdjustment adjustment = start.getStructure().terrainAdaptation();
|
||||
if (!seenStarts.add(start)) {
|
||||
continue;
|
||||
}
|
||||
BoundingBox firstBounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BlockPos firstCenter = firstBounds.getCenter();
|
||||
BlockPos referencePosition = new BlockPos(
|
||||
@@ -251,19 +250,15 @@ public final class NativeStructureSurfaceFitter {
|
||||
if (piece instanceof PoolElementStructurePiece poolPiece) {
|
||||
StructureTemplatePool.Projection projection =
|
||||
poolPiece.getElement().getProjection();
|
||||
if (projection == StructureTemplatePool.Projection.RIGID) {
|
||||
BoundingBox bounds = poolPiece.getBoundingBox();
|
||||
anchors.add(surfaceAnchor(
|
||||
bounds, bounds.minY() + poolPiece.getGroundLevelDelta(),
|
||||
2, adjustment));
|
||||
} else if (!budgetExceeded
|
||||
&& projection == StructureTemplatePool.Projection.TERRAIN_MATCHING
|
||||
if (!budgetExceeded
|
||||
&& (projection == StructureTemplatePool.Projection.RIGID
|
||||
|| projection == StructureTemplatePool.Projection.TERRAIN_MATCHING)
|
||||
&& intersectsHorizontally(poolPiece.getBoundingBox(), influenceArea)) {
|
||||
try {
|
||||
List<SurfaceAnchor> pieceAnchors = new ArrayList<>();
|
||||
addTerrainMatchingSurfaceAnchors(
|
||||
addProcessedSurfaceAnchors(
|
||||
world, influenceArea, poolPiece, referencePosition,
|
||||
adjustment, budget, pieceAnchors);
|
||||
projection, budget, pieceAnchors);
|
||||
anchors.addAll(pieceAnchors);
|
||||
} catch (TemplateBudgetExceeded ignored) {
|
||||
budgetExceeded = true;
|
||||
@@ -274,22 +269,22 @@ public final class NativeStructureSurfaceFitter {
|
||||
anchors.add(new SurfaceAnchor(
|
||||
junction.getSourceX(), junction.getSourceX(),
|
||||
junction.getSourceZ(), junction.getSourceZ(),
|
||||
junction.getSourceGroundY() - 1, 1,
|
||||
junction.getSourceGroundY(), junction.getSourceGroundY()));
|
||||
junction.getSourceGroundY() - 1, 1));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
BoundingBox bounds = piece.getBoundingBox();
|
||||
anchors.add(surfaceAnchor(bounds, bounds.minY(), 2, adjustment));
|
||||
anchors.add(surfaceAnchor(bounds, bounds.minY(), 2));
|
||||
}
|
||||
}
|
||||
return List.copyOf(anchors);
|
||||
}
|
||||
|
||||
private static void addTerrainMatchingSurfaceAnchors(
|
||||
private static void addProcessedSurfaceAnchors(
|
||||
WorldGenLevel world, BoundingBox influenceArea,
|
||||
PoolElementStructurePiece piece, BlockPos referencePosition,
|
||||
TerrainAdjustment adjustment, TemplateCellBudget budget,
|
||||
StructureTemplatePool.Projection projection,
|
||||
TemplateCellBudget budget,
|
||||
List<SurfaceAnchor> anchors) {
|
||||
NativeStructureTemplateOccupancy.OccupancyResult occupancy =
|
||||
NativeStructureTemplateOccupancy.resolve(
|
||||
@@ -299,14 +294,21 @@ public final class NativeStructureSurfaceFitter {
|
||||
if (!occupancy.resolved()) {
|
||||
return;
|
||||
}
|
||||
int groundY = piece.getBoundingBox().minY() + piece.getGroundLevelDelta();
|
||||
int maximumFoundationY = piece.getBoundingBox().minY()
|
||||
+ piece.getGroundLevelDelta();
|
||||
Map<Long, NativeStructureTemplateOccupancy.LowestCell> lowest =
|
||||
NativeStructureTemplateOccupancy.lowestProcessedSolidCells(occupancy.cells());
|
||||
for (NativeStructureTemplateOccupancy.LowestCell cell : lowest.values()) {
|
||||
budget.consume(1);
|
||||
if (projection == StructureTemplatePool.Projection.RIGID
|
||||
&& cell.y() > maximumFoundationY) {
|
||||
continue;
|
||||
}
|
||||
int groundY = projection == StructureTemplatePool.Projection.RIGID
|
||||
? maximumFoundationY : cell.y();
|
||||
BoundingBox column = new BoundingBox(
|
||||
cell.x(), groundY, cell.z(), cell.x(), groundY, cell.z());
|
||||
anchors.add(surfaceAnchor(column, groundY, 2, adjustment));
|
||||
anchors.add(surfaceAnchor(column, groundY, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +320,7 @@ public final class NativeStructureSurfaceFitter {
|
||||
if (WARNED_SOURCE_BUDGET.add(structureId)) {
|
||||
IrisLogging.warn("Native structure SOURCE fitting for '"
|
||||
+ structureId + "' exceeded its bounded template budget; "
|
||||
+ "skipping remaining terrain-matching footprints for this start");
|
||||
+ "skipping remaining processed footprints for this start");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,16 +464,9 @@ public final class NativeStructureSurfaceFitter {
|
||||
return first.surfaceY() <= second.surfaceY() ? first : second;
|
||||
}
|
||||
|
||||
static SurfaceAnchor surfaceAnchor(BoundingBox bounds, int groundY, int strength,
|
||||
TerrainAdjustment adjustment) {
|
||||
int meetY = groundY - 1;
|
||||
if (adjustment == TerrainAdjustment.BEARD_BOX) {
|
||||
return new SurfaceAnchor(
|
||||
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
|
||||
meetY, strength, groundY, bounds.maxY());
|
||||
}
|
||||
static SurfaceAnchor surfaceAnchor(BoundingBox bounds, int groundY, int strength) {
|
||||
return new SurfaceAnchor(bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
|
||||
meetY, strength, groundY, groundY);
|
||||
groundY - 1, strength);
|
||||
}
|
||||
|
||||
static boolean requiresSurfaceTerrain(StructureStart start) {
|
||||
@@ -823,16 +818,7 @@ public final class NativeStructureSurfaceFitter {
|
||||
return state.isSolid() && !NativeStructureVegetationClearer.isTreeBlock(state);
|
||||
}
|
||||
|
||||
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength,
|
||||
int minInfluenceY, int maxInfluenceY) {
|
||||
SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
|
||||
this(minX, maxX, minZ, maxZ, meetY, strength, meetY + 1, meetY + 1);
|
||||
}
|
||||
|
||||
int verticalDistance(int y) {
|
||||
return IrisObjectVacuum.outset(y, minInfluenceY, maxInfluenceY);
|
||||
}
|
||||
|
||||
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
|
||||
}
|
||||
|
||||
private record SurfaceMaterials(BlockState surface, BlockState subsurface) {
|
||||
|
||||
+87
-17
@@ -21,48 +21,118 @@ package art.arcane.iris.modded.service;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class ModdedSettingsHotloadService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long POLL_PERIOD_MILLIS = 3_000L;
|
||||
private static final long POLL_PERIOD_MILLIS = 500L;
|
||||
private static final long HOTLOAD_COOLDOWN_MILLIS = 3_000L;
|
||||
private static final int MAX_SETTINGS_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
private long lastPollAt;
|
||||
private long lastModified;
|
||||
private ConfigHotloadEngine hotloadEngine;
|
||||
private long lastPollAtNanos;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
lastPollAt = 0L;
|
||||
lastModified = settingsFile().lastModified();
|
||||
File settingsFile = settingsFile();
|
||||
hotloadEngine = new ConfigHotloadEngine(
|
||||
this::isSettingsFile,
|
||||
() -> List.of(settingsFile),
|
||||
this::readSettings,
|
||||
this::normalizeSettings
|
||||
);
|
||||
hotloadEngine.configure(
|
||||
POLL_PERIOD_MILLIS,
|
||||
HOTLOAD_COOLDOWN_MILLIS,
|
||||
List.of(settingsFile),
|
||||
List.of()
|
||||
);
|
||||
lastPollAtNanos = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ConfigHotloadEngine active = hotloadEngine;
|
||||
hotloadEngine = null;
|
||||
if (active != null) {
|
||||
active.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastPollAt < POLL_PERIOD_MILLIS) {
|
||||
ConfigHotloadEngine active = hotloadEngine;
|
||||
if (active == null) {
|
||||
return;
|
||||
}
|
||||
lastPollAt = now;
|
||||
long modified = settingsFile().lastModified();
|
||||
if (modified == lastModified) {
|
||||
|
||||
long now = System.nanoTime();
|
||||
if (now - lastPollAtNanos < TimeUnit.MILLISECONDS.toNanos(POLL_PERIOD_MILLIS)) {
|
||||
return;
|
||||
}
|
||||
lastPollAtNanos = now;
|
||||
|
||||
try {
|
||||
for (ConfigHotloadEngine.StableContentSnapshot snapshot : active.pollTouchedSnapshots()) {
|
||||
if ("missing".equals(snapshot.signature())) {
|
||||
active.processSnapshotChange(snapshot, ignored -> true, null);
|
||||
LOGGER.warn("settings.json was removed; retaining the last valid runtime settings");
|
||||
continue;
|
||||
}
|
||||
active.processSnapshotChange(snapshot, this::reloadSettings, ignored -> LOGGER.info("Hotloaded settings.json"));
|
||||
}
|
||||
IrisLanguage.update();
|
||||
return;
|
||||
} catch (RuntimeException failure) {
|
||||
LOGGER.error("Iris settings hotload watcher failed", failure);
|
||||
}
|
||||
if (IrisSettings.settings != null) {
|
||||
IrisSettings.invalidate();
|
||||
}
|
||||
IrisSettings.get();
|
||||
IrisLanguage.reload();
|
||||
lastModified = settingsFile().lastModified();
|
||||
LOGGER.info("Hotloaded settings.json");
|
||||
|
||||
private boolean reloadSettings(ConfigHotloadEngine.StableContentSnapshot snapshot) {
|
||||
try {
|
||||
String content = snapshot.normalizedContent();
|
||||
if (content == null) {
|
||||
return false;
|
||||
}
|
||||
return IrisSettings.applyHotloadSnapshot(content, IrisLanguage::reload);
|
||||
} catch (RuntimeException failure) {
|
||||
LOGGER.error("Iris settings hotload failed; keeping the previous runtime settings", failure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSettingsFile(File file) {
|
||||
return file != null && settingsFile().getAbsoluteFile().equals(file.getAbsoluteFile());
|
||||
}
|
||||
|
||||
private String readSettings(File file) {
|
||||
if (file == null || !file.isFile()) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream input = Files.newInputStream(file.toPath())) {
|
||||
byte[] content = input.readNBytes(MAX_SETTINGS_BYTES + 1);
|
||||
if (content.length > MAX_SETTINGS_BYTES) {
|
||||
throw new IOException("Settings exceed " + MAX_SETTINGS_BYTES + " bytes: " + file);
|
||||
}
|
||||
return new String(content, StandardCharsets.UTF_8);
|
||||
} catch (IOException failure) {
|
||||
throw new UncheckedIOException("Failed to read Iris settings from " + file, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeSettings(String content) {
|
||||
return content == null ? null : content.replace("\r\n", "\n").trim();
|
||||
}
|
||||
|
||||
private static File settingsFile() {
|
||||
|
||||
+22
-1
@@ -43,6 +43,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -82,6 +83,9 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
if (active != null) {
|
||||
active.shutdownNow();
|
||||
}
|
||||
for (Watch watch : watches.values()) {
|
||||
watch.close();
|
||||
}
|
||||
watches.clear();
|
||||
}
|
||||
|
||||
@@ -172,7 +176,13 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
}
|
||||
if (!watches.isEmpty()) {
|
||||
Set<String> keep = seen == null ? Set.of() : seen;
|
||||
watches.keySet().removeIf((String key) -> !keep.contains(key));
|
||||
watches.entrySet().removeIf((Map.Entry<String, Watch> entry) -> {
|
||||
if (keep.contains(entry.getKey())) {
|
||||
return false;
|
||||
}
|
||||
entry.getValue().close();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +222,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
try {
|
||||
if (watch.engine != engine) {
|
||||
watch.close();
|
||||
watch.engine = engine;
|
||||
watch.folder = new ReactiveFolder(
|
||||
engine.getData().getDataFolder(),
|
||||
@@ -241,6 +252,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||
throw new IllegalStateException("Iris studio hotload failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,5 +284,14 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
private volatile Engine engine;
|
||||
private volatile ReactiveFolder folder;
|
||||
|
||||
private void close() {
|
||||
ReactiveFolder active = folder;
|
||||
folder = null;
|
||||
if (active != null) {
|
||||
active.clear();
|
||||
}
|
||||
engine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.59'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').get
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.2-65.1.1')
|
||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.2.0.59')
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||
.get()
|
||||
String useLocalVolmLib = providers.gradleProperty('useLocalVolmLib').getOrElse('true')
|
||||
String localVolmLibDirectory = providers.gradleProperty('localVolmLibDirectory').getOrNull()
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ plugins {
|
||||
|
||||
def lib = 'art.arcane.iris.util'
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||
.get()
|
||||
String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN')
|
||||
boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank()
|
||||
|
||||
@@ -63,7 +63,6 @@ art/arcane/iris/core/service/ExternalDataSVC.java
|
||||
art/arcane/iris/core/service/GlobalCacheSVC.java
|
||||
art/arcane/iris/core/service/JigsawStudioMarkerParser.java
|
||||
art/arcane/iris/core/service/JigsawStudioMenuController.java
|
||||
art/arcane/iris/core/service/JigsawStudioBoundsRenderer.java
|
||||
art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java
|
||||
art/arcane/iris/core/service/JigsawStudioService.java
|
||||
art/arcane/iris/core/service/JigsawStudioToolCodec.java
|
||||
|
||||
@@ -30,6 +30,8 @@ import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Data
|
||||
public class IrisSettings {
|
||||
@@ -136,6 +138,46 @@ public class IrisSettings {
|
||||
}
|
||||
}
|
||||
|
||||
public static IrisSettings installHotloadSnapshot(String rawJson) {
|
||||
IrisSettings parsed = parseHotloadSnapshot(rawJson);
|
||||
synchronized (SETTINGS_LOCK) {
|
||||
settings = parsed;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public static boolean applyHotloadSnapshot(String rawJson, Predicate<IrisSettings> candidateActivation) {
|
||||
Objects.requireNonNull(candidateActivation, "candidateActivation");
|
||||
IrisSettings parsed = parseHotloadSnapshot(rawJson);
|
||||
if (!candidateActivation.test(parsed)) {
|
||||
return false;
|
||||
}
|
||||
synchronized (SETTINGS_LOCK) {
|
||||
settings = parsed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IrisSettings parseHotloadSnapshot(String rawJson) {
|
||||
if (rawJson == null || rawJson.isBlank()) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot is empty");
|
||||
}
|
||||
|
||||
IrisSettings parsed;
|
||||
try {
|
||||
parsed = new Gson().fromJson(rawJson, IrisSettings.class);
|
||||
if (parsed == null) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot did not contain an object");
|
||||
}
|
||||
migrateLegacyKeys(parsed, rawJson);
|
||||
} catch (RuntimeException failure) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot is invalid", failure);
|
||||
}
|
||||
|
||||
parsed.fillMissingSections();
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void forceSave() {
|
||||
File s = IrisPlatforms.get().dataFile("settings.json");
|
||||
|
||||
@@ -147,6 +189,20 @@ public class IrisSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMissingSections() {
|
||||
general = general == null ? new IrisSettingsGeneral() : general;
|
||||
world = world == null ? new IrisSettingsWorld() : world;
|
||||
gui = gui == null ? new IrisSettingsGUI() : gui;
|
||||
autoConfiguration = autoConfiguration == null ? new IrisSettingsAutoconfiguration() : autoConfiguration;
|
||||
generator = generator == null ? new IrisSettingsGenerator() : generator;
|
||||
concurrency = concurrency == null ? new IrisSettingsConcurrency() : concurrency;
|
||||
studio = studio == null ? new IrisSettingsStudio() : studio;
|
||||
performance = performance == null ? new IrisSettingsPerformance() : performance;
|
||||
pregen = pregen == null ? new IrisSettingsPregen() : pregen;
|
||||
sentry = sentry == null ? new IrisSettingsSentry() : sentry;
|
||||
treeFeller = treeFeller == null ? new IrisSettingsTreeFeller() : treeFeller;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsAutoconfiguration {
|
||||
public boolean configureSpigotTimeoutTime = true;
|
||||
@@ -273,6 +329,11 @@ public class IrisSettings {
|
||||
public boolean splashLogoStartup = true;
|
||||
public boolean useConsoleCustomColors = true;
|
||||
public boolean useCustomColorsIngame = true;
|
||||
/**
|
||||
* Boss bar progress loaders for jobs, studio opens, world creation, chunk jobs and pack
|
||||
* downloads. Turning this off keeps the action bar progress line; only the bar goes away.
|
||||
*/
|
||||
public boolean progressBossBar = true;
|
||||
public boolean adjustVanillaHeight = false;
|
||||
public boolean autoIngestDatapacks = true;
|
||||
/**
|
||||
|
||||
@@ -28,19 +28,32 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class IrisLanguage {
|
||||
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
||||
private static final long HOTLOAD_CONTENT_STABILITY_NANOS = TimeUnit.MILLISECONDS.toNanos(250L);
|
||||
private static final long HOTLOAD_DELETION_GRACE_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
||||
private static final long HOTLOAD_COOLDOWN_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
||||
private static final int MAX_REPORTED_ISSUES = 12;
|
||||
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
||||
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
||||
@@ -55,10 +68,17 @@ public final class IrisLanguage {
|
||||
* new snapshot and the whole memo is discarded. Bounded by the catalog size.
|
||||
*/
|
||||
private static final AtomicReference<PlainMemo> PLAIN_MEMO = new AtomicReference<>(null);
|
||||
private static final LocaleHotloadGate HOTLOAD_GATE = new LocaleHotloadGate(
|
||||
new LocaleHotloadGate.Timing(
|
||||
HOTLOAD_CONTENT_STABILITY_NANOS,
|
||||
HOTLOAD_DELETION_GRACE_NANOS,
|
||||
HOTLOAD_COOLDOWN_NANOS
|
||||
)
|
||||
);
|
||||
|
||||
private static volatile File dataFolder;
|
||||
private static volatile File watchedFile;
|
||||
private static volatile long watchedSignature = Long.MIN_VALUE;
|
||||
private static volatile String lastCaptureFailureKey;
|
||||
private static volatile String lastInvalidHotloadLocale;
|
||||
private static volatile String activeLocale = CATALOG.englishLocale();
|
||||
|
||||
private IrisLanguage() {
|
||||
@@ -82,6 +102,20 @@ public final class IrisLanguage {
|
||||
return reload(root, configuredLocale());
|
||||
}
|
||||
|
||||
public static synchronized boolean reload(IrisSettings candidate) {
|
||||
IrisSettings resolvedCandidate = Objects.requireNonNull(candidate, "Candidate settings cannot be null");
|
||||
File root = dataFolder;
|
||||
if (root == null && IrisPlatforms.isBound()) {
|
||||
root = IrisPlatforms.get().dataFolder();
|
||||
}
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
IrisSettings.IrisSettingsGeneral general = resolvedCandidate.getGeneral();
|
||||
String locale = general == null ? CATALOG.englishLocale() : general.getLanguage();
|
||||
return reload(root, locale);
|
||||
}
|
||||
|
||||
public static synchronized boolean reload(File root, String locale) {
|
||||
File resolvedRoot = root == null ? null : root.getAbsoluteFile();
|
||||
if (resolvedRoot == null) {
|
||||
@@ -92,16 +126,26 @@ public final class IrisLanguage {
|
||||
requestedLocale = normalizeLocale(locale);
|
||||
} catch (RuntimeException exception) {
|
||||
dataFolder = resolvedRoot;
|
||||
HOTLOAD_GATE.reset(null);
|
||||
IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + ".");
|
||||
IrisLogging.reportError(exception);
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale));
|
||||
dataFolder = resolvedRoot;
|
||||
|
||||
File override = overrideFile(resolvedRoot, requestedLocale);
|
||||
watchedFile = override;
|
||||
watchedSignature = signature(override);
|
||||
SnapshotCapture capture = captureForReload(override, requestedLocale);
|
||||
LocalizationReloadResult result;
|
||||
if (capture.failure() == null) {
|
||||
result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale, capture.snapshot()));
|
||||
} else {
|
||||
result = MANAGER.reload(() -> {
|
||||
throw capture.failure();
|
||||
});
|
||||
}
|
||||
dataFolder = resolvedRoot;
|
||||
HOTLOAD_GATE.reset(capture.snapshot());
|
||||
lastCaptureFailureKey = null;
|
||||
if (!result.applied()) {
|
||||
reportRejectedReload(requestedLocale, result);
|
||||
return false;
|
||||
@@ -123,14 +167,47 @@ public final class IrisLanguage {
|
||||
try {
|
||||
locale = normalizeLocale(configuredLocale());
|
||||
} catch (RuntimeException exception) {
|
||||
return reload(root, configuredLocale());
|
||||
String invalidLocale = configuredLocale();
|
||||
if (Objects.equals(lastInvalidHotloadLocale, invalidLocale)) {
|
||||
return false;
|
||||
}
|
||||
lastInvalidHotloadLocale = invalidLocale;
|
||||
return reload(root, invalidLocale);
|
||||
}
|
||||
lastInvalidHotloadLocale = null;
|
||||
File expected = overrideFile(root, locale);
|
||||
long signature = signature(expected);
|
||||
if (expected.equals(watchedFile) && signature == watchedSignature) {
|
||||
LocaleHotloadSnapshot snapshot;
|
||||
try {
|
||||
snapshot = captureHotloadSnapshot(expected, locale);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
HOTLOAD_GATE.unavailable();
|
||||
reportCaptureFailure(expected, failure);
|
||||
return false;
|
||||
}
|
||||
if (snapshot == null) {
|
||||
HOTLOAD_GATE.unavailable();
|
||||
return true;
|
||||
}
|
||||
return reload(root, locale);
|
||||
lastCaptureFailureKey = null;
|
||||
|
||||
LocaleHotloadGate.Attempt attempt = HOTLOAD_GATE.observe(snapshot, System.nanoTime());
|
||||
if (attempt == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(root, locale, attempt.snapshot()));
|
||||
boolean applied = result.applied();
|
||||
HOTLOAD_GATE.complete(attempt, System.nanoTime(), applied);
|
||||
if (!applied) {
|
||||
reportRejectedReload(locale, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
activeLocale = locale;
|
||||
int warnings = result.validation().warnings().size();
|
||||
IrisLogging.info("Loaded locale " + locale + " with " + warnings + " fallback "
|
||||
+ (warnings == 1 ? "entry" : "entries") + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String activeLocale() {
|
||||
@@ -229,13 +306,16 @@ public final class IrisLanguage {
|
||||
throw new IllegalArgumentException("Unsupported Iris message key: " + key.id());
|
||||
}
|
||||
|
||||
private static LocalizationCandidate loadCandidate(File root, String locale) throws Exception {
|
||||
private static LocalizationCandidate loadCandidate(
|
||||
File root,
|
||||
String locale,
|
||||
LocaleHotloadSnapshot snapshot
|
||||
) throws Exception {
|
||||
File folder = new File(root, "languages/overrides");
|
||||
Files.createDirectories(folder.toPath());
|
||||
List<LocaleOverlay> overlays = new ArrayList<>(2);
|
||||
File override = overrideFile(root, locale);
|
||||
if (override.exists()) {
|
||||
overlays.add(loadFileOverlay(override, locale));
|
||||
if (!snapshot.missing()) {
|
||||
overlays.add(parseOverlay(snapshot.file().getPath(), locale, snapshot.content()));
|
||||
}
|
||||
|
||||
if (!CATALOG.englishLocale().equals(locale)) {
|
||||
@@ -269,17 +349,6 @@ public final class IrisLanguage {
|
||||
}
|
||||
}
|
||||
|
||||
private static LocaleOverlay loadFileOverlay(File override, String locale) throws Exception {
|
||||
if (!override.isFile()) {
|
||||
throw new IllegalArgumentException("Locale override is not a regular file: " + override.getPath());
|
||||
}
|
||||
if (override.length() > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + override.getPath());
|
||||
}
|
||||
String raw = Files.readString(override.toPath(), StandardCharsets.UTF_8);
|
||||
return parseOverlay(override.getPath(), locale, raw);
|
||||
}
|
||||
|
||||
private static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
||||
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
||||
if (!parsed.isJsonObject()) {
|
||||
@@ -461,11 +530,92 @@ public final class IrisLanguage {
|
||||
return new File(new File(root, "languages/overrides"), normalizeLocale(locale) + ".json").getAbsoluteFile();
|
||||
}
|
||||
|
||||
private static long signature(File file) {
|
||||
if (file == null || !file.exists()) {
|
||||
return 0L;
|
||||
static LocaleHotloadSnapshot captureHotloadSnapshot(File file, String locale) throws IOException {
|
||||
File resolvedFile = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile();
|
||||
String resolvedLocale = normalizeLocale(locale);
|
||||
BasicFileAttributes before;
|
||||
try {
|
||||
before = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return LocaleHotloadSnapshot.missing(resolvedFile, resolvedLocale);
|
||||
}
|
||||
return file.lastModified() * 31L + file.length();
|
||||
if (!before.isRegularFile()) {
|
||||
throw new IllegalArgumentException("Locale override is not a regular file: " + resolvedFile.getPath());
|
||||
}
|
||||
if (before.size() > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath());
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try (InputStream input = Files.newInputStream(resolvedFile.toPath())) {
|
||||
bytes = input.readNBytes((int) MAX_LOCALE_BYTES + 1);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return null;
|
||||
}
|
||||
if (bytes.length > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath());
|
||||
}
|
||||
|
||||
BasicFileAttributes after;
|
||||
try {
|
||||
after = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return null;
|
||||
}
|
||||
if (!sameIdentity(before, after) || bytes.length != after.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String content;
|
||||
try {
|
||||
content = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString();
|
||||
} catch (CharacterCodingException failure) {
|
||||
throw new IOException("Locale override is not valid UTF-8: " + resolvedFile.getPath(), failure);
|
||||
}
|
||||
return LocaleHotloadSnapshot.present(resolvedFile, resolvedLocale, content, sha256(bytes));
|
||||
}
|
||||
|
||||
private static SnapshotCapture captureForReload(File file, String locale) {
|
||||
try {
|
||||
LocaleHotloadSnapshot snapshot = captureHotloadSnapshot(file, locale);
|
||||
if (snapshot == null) {
|
||||
return new SnapshotCapture(
|
||||
null,
|
||||
new IOException("Locale override changed while being read: " + file.getPath())
|
||||
);
|
||||
}
|
||||
return new SnapshotCapture(snapshot, null);
|
||||
} catch (Exception failure) {
|
||||
return new SnapshotCapture(null, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameIdentity(BasicFileAttributes before, BasicFileAttributes after) {
|
||||
return before.isRegularFile() == after.isRegularFile()
|
||||
&& before.size() == after.size()
|
||||
&& before.lastModifiedTime().equals(after.lastModifiedTime())
|
||||
&& Objects.equals(before.fileKey(), after.fileKey());
|
||||
}
|
||||
|
||||
private static String sha256(byte[] content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(digest.digest(content));
|
||||
} catch (NoSuchAlgorithmException failure) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void reportCaptureFailure(File file, Exception failure) {
|
||||
String detail = failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
|
||||
String failureKey = file.getAbsolutePath() + ":" + failure.getClass().getName() + ":" + detail;
|
||||
if (Objects.equals(failureKey, lastCaptureFailureKey)) {
|
||||
return;
|
||||
}
|
||||
lastCaptureFailureKey = failureKey;
|
||||
IrisLogging.error("Failed to capture stable locale override " + file.getPath() + ": " + detail);
|
||||
IrisLogging.reportError(failure);
|
||||
failure.printStackTrace();
|
||||
}
|
||||
|
||||
private static void reportRejectedReload(String locale, LocalizationReloadResult result) {
|
||||
@@ -489,4 +639,7 @@ public final class IrisLanguage {
|
||||
|
||||
private record PlainMemo(LocalizationSnapshot snapshot, Map<String, String> values) {
|
||||
}
|
||||
|
||||
private record SnapshotCapture(LocaleHotloadSnapshot snapshot, Exception failure) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class LocaleHotloadGate {
|
||||
private final Timing timing;
|
||||
|
||||
private long generation;
|
||||
private LocaleHotloadSnapshot baseline;
|
||||
private LocaleHotloadSnapshot observed;
|
||||
private long observedSinceNanos;
|
||||
private LocaleHotloadSnapshot pending;
|
||||
private Attempt inFlight;
|
||||
private long lastCompletedAtNanos;
|
||||
private boolean hasCompleted;
|
||||
|
||||
LocaleHotloadGate(Timing timing) {
|
||||
this.timing = Objects.requireNonNull(timing, "Locale hotload timing cannot be null");
|
||||
}
|
||||
|
||||
synchronized Attempt observe(LocaleHotloadSnapshot snapshot, long nowNanos) {
|
||||
LocaleHotloadSnapshot current = Objects.requireNonNull(snapshot, "Locale hotload snapshot cannot be null");
|
||||
if (!current.equals(observed)) {
|
||||
observed = current;
|
||||
observedSinceNanos = nowNanos;
|
||||
return null;
|
||||
}
|
||||
|
||||
long stabilityNanos = current.missing()
|
||||
? timing.deletionGraceNanos()
|
||||
: timing.contentStabilityNanos();
|
||||
if (!elapsed(nowNanos, observedSinceNanos, stabilityNanos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pending = current.equals(baseline) ? null : current;
|
||||
if (pending == null || inFlight != null || !pending.equals(observed)) {
|
||||
return null;
|
||||
}
|
||||
if (hasCompleted && !elapsed(nowNanos, lastCompletedAtNanos, timing.cooldownNanos())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Attempt attempt = new Attempt(generation, pending);
|
||||
inFlight = attempt;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
synchronized void unavailable() {
|
||||
observed = null;
|
||||
observedSinceNanos = 0L;
|
||||
}
|
||||
|
||||
synchronized void complete(Attempt attempt, long nowNanos, boolean applied) {
|
||||
if (attempt == null || inFlight == null || !inFlight.equals(attempt) || attempt.generation() != generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
inFlight = null;
|
||||
lastCompletedAtNanos = nowNanos;
|
||||
hasCompleted = true;
|
||||
if (!applied) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseline = attempt.snapshot();
|
||||
if (attempt.snapshot().equals(pending)) {
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void reset(LocaleHotloadSnapshot snapshot) {
|
||||
generation++;
|
||||
baseline = snapshot;
|
||||
observed = snapshot;
|
||||
observedSinceNanos = 0L;
|
||||
pending = null;
|
||||
inFlight = null;
|
||||
lastCompletedAtNanos = 0L;
|
||||
hasCompleted = false;
|
||||
}
|
||||
|
||||
private boolean elapsed(long nowNanos, long startNanos, long durationNanos) {
|
||||
return nowNanos - startNanos >= durationNanos;
|
||||
}
|
||||
|
||||
record Timing(long contentStabilityNanos, long deletionGraceNanos, long cooldownNanos) {
|
||||
Timing {
|
||||
if (contentStabilityNanos < 0L || deletionGraceNanos < 0L || cooldownNanos < 0L) {
|
||||
throw new IllegalArgumentException("Locale hotload timing cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Attempt(long generation, LocaleHotloadSnapshot snapshot) {
|
||||
Attempt {
|
||||
snapshot = Objects.requireNonNull(snapshot, "Locale hotload attempt snapshot cannot be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
record LocaleHotloadSnapshot(File file, String locale, String content, String sha256) {
|
||||
LocaleHotloadSnapshot {
|
||||
file = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile();
|
||||
locale = Objects.requireNonNull(locale, "Locale cannot be null");
|
||||
sha256 = Objects.requireNonNull(sha256, "Locale content hash cannot be null");
|
||||
}
|
||||
|
||||
static LocaleHotloadSnapshot missing(File file, String locale) {
|
||||
return new LocaleHotloadSnapshot(file, locale, null, "missing");
|
||||
}
|
||||
|
||||
static LocaleHotloadSnapshot present(File file, String locale, String content, String sha256) {
|
||||
return new LocaleHotloadSnapshot(
|
||||
file,
|
||||
locale,
|
||||
Objects.requireNonNull(content, "Locale content cannot be null"),
|
||||
sha256
|
||||
);
|
||||
}
|
||||
|
||||
boolean missing() {
|
||||
return content == null;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
@@ -46,7 +47,8 @@ final class StudioOpenProgressReporter {
|
||||
AtomicInteger taskId = new AtomicInteger(-1);
|
||||
org.bukkit.boss.BossBar bossBar;
|
||||
|
||||
if (sender.isPlayer() && sender.player() != null) {
|
||||
if (sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
bossBar = Bukkit.createBossBar(
|
||||
IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING),
|
||||
org.bukkit.boss.BarColor.BLUE,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -109,8 +110,9 @@ public final class ChunkJobReporter {
|
||||
}
|
||||
|
||||
private void startReporter() {
|
||||
boolean player = sender.isPlayer() && sender.player() != null;
|
||||
BossBar bossBar = player
|
||||
boolean showBossBar = sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar();
|
||||
BossBar bossBar = showBossBar
|
||||
? Bukkit.createBossBar(IrisLanguage.text(
|
||||
RuntimeProgressMessages.CHUNK_BOSSBAR_WORKING,
|
||||
MessageArgument.trusted("title", title)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
@@ -246,6 +247,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
|
||||
private void renderHudPulse(HudSnapshot snapshot) {
|
||||
try {
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
@@ -255,6 +257,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
BarStyle.SEGMENTED_20,
|
||||
1_500L
|
||||
);
|
||||
}
|
||||
sender.sendAction(snapshot.line());
|
||||
} catch (RuntimeException failure) {
|
||||
disableHud(failure);
|
||||
@@ -310,6 +313,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
Runnable retiredCleanup = () -> retireHudLane(cleaned);
|
||||
Runnable display = () -> {
|
||||
try {
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
@@ -319,6 +323,7 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
BarStyle.SOLID,
|
||||
4_000L
|
||||
);
|
||||
}
|
||||
sender.sendAction(message);
|
||||
} finally {
|
||||
if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) {
|
||||
|
||||
@@ -488,33 +488,50 @@ public class StudioSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean blockIfPackBroken(VolmitSender sender, String dimm) {
|
||||
private static BrokenPackException reportPackAdmissionFailure(
|
||||
VolmitSender sender, String dimm) {
|
||||
Optional<String> startupDenial = IrisStartupValidation.denialReason();
|
||||
if (startupDenial.isPresent()) {
|
||||
sender.sendMessage(startupDenial.get());
|
||||
return true;
|
||||
}
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(dimm);
|
||||
String packName = dimension == null || dimension.getLoader() == null
|
||||
? dimm
|
||||
: dimension.getLoader().getDataFolder().getName();
|
||||
PackValidationResult validation = PackValidationRegistry.get(packName);
|
||||
if (validation != null && validation.isLoadable()) {
|
||||
return false;
|
||||
BrokenPackException failure = resolvePackAdmissionFailure(
|
||||
packName, startupDenial, validation);
|
||||
if (failure == null) {
|
||||
return null;
|
||||
}
|
||||
if (startupDenial.isPresent()) {
|
||||
sender.sendMessage(startupDenial.get());
|
||||
return failure;
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS, MessageArgument.untrusted("dimm", String.valueOf(dimm))));
|
||||
for (String reason : failure.getReasons()) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("dimm", String.valueOf(packName))));
|
||||
return failure;
|
||||
}
|
||||
|
||||
static BrokenPackException resolvePackAdmissionFailure(
|
||||
String packName,
|
||||
Optional<String> startupDenial,
|
||||
PackValidationResult validation
|
||||
) {
|
||||
if (startupDenial.isPresent()) {
|
||||
return new BrokenPackException(packName, List.of(startupDenial.get()));
|
||||
}
|
||||
if (validation != null && validation.isLoadable()) {
|
||||
return null;
|
||||
}
|
||||
List<String> failures = validation == null
|
||||
? List.of("Required pack validation has not completed. Studio creation fails closed until validation succeeds.")
|
||||
: validation.getBlockingErrors();
|
||||
for (String reason : failures) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("dimm", String.valueOf(dimm))));
|
||||
return true;
|
||||
return new BrokenPackException(packName, failures);
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
|
||||
if (blockIfPackBroken(sender, dimm)) {
|
||||
if (reportPackAdmissionFailure(sender, dimm) != null) {
|
||||
return;
|
||||
}
|
||||
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, onDone))
|
||||
@@ -537,9 +554,9 @@ public class StudioSVC implements IrisService {
|
||||
Runnable beforeOpen,
|
||||
Consumer<World> onDone
|
||||
) {
|
||||
if (blockIfPackBroken(sender, dimension)) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Studio pack '" + dimension + "' has blocking validation errors."));
|
||||
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
|
||||
if (failure != null) {
|
||||
return CompletableFuture.failedFuture(failure);
|
||||
}
|
||||
return studioTransitions.submit(() -> replaceActiveProjectTracked(
|
||||
sender,
|
||||
|
||||
@@ -426,7 +426,7 @@ public final class VillageImporter {
|
||||
emittedPools.size(), emittedPieces.size(), losses, true);
|
||||
}
|
||||
|
||||
String msg = "Imported village " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
|
||||
String msg = "Imported jigsaw structure " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
|
||||
if (!losses.isEmpty()) {
|
||||
msg += " (" + losses.size() + " fidelity warning(s) recorded)";
|
||||
}
|
||||
|
||||
@@ -624,11 +624,13 @@ public class IrisCreator {
|
||||
return;
|
||||
}
|
||||
if (showLoaderHud) {
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text(
|
||||
RuntimeProgressMessages.WORLD_PREGEN_ACTION,
|
||||
MessageArgument.trusted("bar", ""),
|
||||
MessageArgument.trusted("percent", percent)
|
||||
), p, BarColor.GREEN, BarStyle.SOLID, 4000L);
|
||||
}
|
||||
int barWidth = 44;
|
||||
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth);
|
||||
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -78,7 +79,8 @@ final class WorldCreationProgressReporter {
|
||||
|
||||
static WorldCreationProgressReporter start(VolmitSender sender, String worldName) {
|
||||
WorldCreationProgressReporter reporter = new WorldCreationProgressReporter(sender, worldName);
|
||||
if (sender.isPlayer() && sender.player() != null) {
|
||||
if (sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
try {
|
||||
J.sfut(reporter::initializePlayerHud).get(5L, TimeUnit.SECONDS);
|
||||
} catch (Throwable failure) {
|
||||
|
||||
@@ -281,8 +281,8 @@ public final class IrisObjectIO {
|
||||
AtomicReference<IOException> ref = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Job() {
|
||||
private int total = self.blocks.size() * 3 + self.states.size();
|
||||
private int c = 0;
|
||||
private volatile int total = self.blocks.size() * 3 + self.states.size();
|
||||
private volatile int c = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
||||
@@ -147,7 +147,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.folder = new ReactiveFolder(
|
||||
dataLocation,
|
||||
(_a, _b, _c) -> hotload(),
|
||||
(_a, _b, _c) -> hotloadFromWatcher(),
|
||||
new KList<>(".iob", ".json"),
|
||||
new KList<>(".iris"),
|
||||
new KList<>()
|
||||
@@ -542,6 +542,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
withExclusiveControl(() -> getEngine().hotload());
|
||||
}
|
||||
|
||||
private void hotloadFromWatcher() {
|
||||
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
return;
|
||||
}
|
||||
withExclusiveControlFuture(() -> getEngine().hotload(), 30L, TimeUnit.SECONDS).join();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) {
|
||||
Engine activeEngine = getEngine();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.MinecraftVersion;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
@@ -161,6 +162,9 @@ public final class BukkitPlatform implements IrisPlatform {
|
||||
}
|
||||
|
||||
public static void showProgressLane(Player player, String laneId, String title, double progress, long staleMillis) {
|
||||
if (!IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
return;
|
||||
}
|
||||
hudLanes().show(player, laneId, title, progress, BarColor.BLUE, BarStyle.SOLID, staleMillis);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ import java.net.URI;
|
||||
|
||||
public class DownloadJob implements Job {
|
||||
private final DL.Download download;
|
||||
private int tw;
|
||||
private int cw;
|
||||
private volatile int tw;
|
||||
private volatile int cw;
|
||||
|
||||
public DownloadJob(String url, File destination) throws MalformedURLException {
|
||||
tw = 1;
|
||||
|
||||
@@ -23,7 +23,7 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
public class JobCollection implements Job {
|
||||
private final String name;
|
||||
private final KList<Job> jobs;
|
||||
private String status;
|
||||
private volatile String status;
|
||||
|
||||
public JobCollection(String name, Job... jobs) {
|
||||
this(name, new KList<>(jobs));
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public abstract class QueueJob<T> implements Job {
|
||||
final KList<T> queue;
|
||||
private final AtomicInteger completed;
|
||||
protected int totalWork;
|
||||
protected volatile int totalWork;
|
||||
|
||||
public QueueJob() {
|
||||
totalWork = 0;
|
||||
|
||||
@@ -21,7 +21,7 @@ package art.arcane.iris.util.common.scheduling.jobs;
|
||||
public class SingleJob implements Job {
|
||||
private final String name;
|
||||
private final Runnable runnable;
|
||||
private boolean done;
|
||||
private volatile boolean done;
|
||||
|
||||
public SingleJob(String name, Runnable runnable) {
|
||||
this.name = name;
|
||||
|
||||
@@ -6,7 +6,10 @@ import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisSettingsDefaultsTest {
|
||||
@Test
|
||||
@@ -34,4 +37,64 @@ public class IrisSettingsDefaultsTest {
|
||||
assertTrue(settings.isAutoIngestDatapacks());
|
||||
assertFalse(settings.isAutoImportDatapackStructures());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadSnapshotIsValidatedBeforeReplacingLiveSettings() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings live = new IrisSettings();
|
||||
IrisSettings.settings = live;
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> IrisSettings.installHotloadSnapshot("{\"general\":"));
|
||||
assertSame(live, IrisSettings.settings);
|
||||
|
||||
IrisSettings installed = IrisSettings.installHotloadSnapshot(
|
||||
"{\"general\":{\"language\":\"fr_FR\"},\"world\":null}");
|
||||
assertSame(installed, IrisSettings.settings);
|
||||
assertEquals("fr_FR", installed.getGeneral().getLanguage());
|
||||
assertNotNull(installed.getWorld());
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedHotloadActivationKeepsPreviousLiveSettings() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings live = new IrisSettings();
|
||||
live.getGeneral().setLanguage("en_US");
|
||||
IrisSettings.settings = live;
|
||||
try {
|
||||
boolean applied = IrisSettings.applyHotloadSnapshot(
|
||||
"{\"general\":{\"language\":\"de_DE\"}}",
|
||||
candidate -> false
|
||||
);
|
||||
|
||||
assertFalse(applied);
|
||||
assertSame(live, IrisSettings.settings);
|
||||
assertEquals("en_US", IrisSettings.settings.getGeneral().getLanguage());
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void successfulHotloadActivationPublishesCandidateSettings() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings live = new IrisSettings();
|
||||
IrisSettings.settings = live;
|
||||
try {
|
||||
boolean applied = IrisSettings.applyHotloadSnapshot(
|
||||
"{\"general\":{\"language\":\"de_DE\"},\"world\":null}",
|
||||
candidate -> "de_DE".equals(candidate.getGeneral().getLanguage())
|
||||
);
|
||||
|
||||
assertTrue(applied);
|
||||
assertNotSame(live, IrisSettings.settings);
|
||||
assertEquals("de_DE", IrisSettings.settings.getGeneral().getLanguage());
|
||||
assertNotNull(IrisSettings.settings.getWorld());
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ProgressBossBarToggleTest {
|
||||
@Test
|
||||
public void progressBossBarsAreEnabledByDefault() {
|
||||
assertTrue(new IrisSettings.IrisSettingsGeneral().isProgressBossBar());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void showProgressLaneNeverTouchesTheLaneServiceWhenDisabled() {
|
||||
withProgressBossBar(false, () ->
|
||||
BukkitPlatform.showProgressLane(null, "iris:job", "Working", 0.5D, 4000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void showProgressLaneStillReachesTheLaneServiceWhenEnabled() {
|
||||
withProgressBossBar(true, () -> assertThrows(IllegalStateException.class,
|
||||
() -> BukkitPlatform.showProgressLane(null, "iris:job", "Working", 0.5D, 4000L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioOpenBossBarIsGatedBySettings() throws Exception {
|
||||
assertGated("core/project/StudioOpenProgressReporter.java");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worldCreationBossBarIsGatedBySettings() throws Exception {
|
||||
assertGated("core/tools/WorldCreationProgressReporter.java");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkJobBossBarIsGatedBySettings() throws Exception {
|
||||
assertGated("core/runtime/ChunkJobReporter.java");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packDownloadLaneIsGatedWhileTheActionBarSurvives() throws Exception {
|
||||
String source = source("core/service/PackDownloadProgressReporter.java");
|
||||
|
||||
assertTrue(source.contains("isProgressBossBar()"));
|
||||
assertTrue("the action bar must keep reporting when boss bars are off",
|
||||
source.contains("sender.sendAction(snapshot.line())"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenLaneIsGatedWhileTheActionBarSurvives() throws Exception {
|
||||
String source = source("core/tools/IrisCreator.java");
|
||||
int lane = source.indexOf("\"iris:pregen\"");
|
||||
int gate = source.lastIndexOf("isProgressBossBar()", lane);
|
||||
|
||||
assertTrue("the pregen boss bar lane must sit behind general.progressBossBar",
|
||||
gate > 0 && lane - gate < 200);
|
||||
assertTrue("the action bar must keep reporting when boss bars are off",
|
||||
source.contains("RuntimeProgressMessages.WORLD_PREGEN_ACTION"));
|
||||
}
|
||||
|
||||
private void assertGated(String relativePath) throws Exception {
|
||||
assertTrue(relativePath + " must consult general.progressBossBar before creating a boss bar",
|
||||
source(relativePath).contains("isProgressBossBar()"));
|
||||
}
|
||||
|
||||
private String source(String relativePath) throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/art/arcane/iris").resolve(relativePath));
|
||||
}
|
||||
|
||||
private void withProgressBossBar(boolean enabled, Runnable body) {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
try {
|
||||
IrisSettings live = new IrisSettings();
|
||||
live.getGeneral().setProgressBossBar(enabled);
|
||||
IrisSettings.settings = live;
|
||||
assertFalse("another test hosted a HUD; this test needs an unhosted platform",
|
||||
BukkitPlatform.hasHud());
|
||||
body.run();
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisLanguageHotloadSnapshotTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void capturesMissingOverrideAsStableTombstone() throws Exception {
|
||||
File override = new File(temporaryFolder.getRoot(), "languages/overrides/en_US.json");
|
||||
|
||||
LocaleHotloadSnapshot snapshot = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
|
||||
assertTrue(snapshot.missing());
|
||||
assertEquals("missing", snapshot.sha256());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detectsSameMetadataContentReplacementBySha256() throws Exception {
|
||||
File override = new File(temporaryFolder.getRoot(), "languages/overrides/en_US.json");
|
||||
Files.createDirectories(override.toPath().getParent());
|
||||
String firstContent = "{\"messages\":{\"a\":\"1\"}}";
|
||||
String secondContent = "{\"messages\":{\"a\":\"2\"}}";
|
||||
Files.writeString(override.toPath(), firstContent, StandardCharsets.UTF_8);
|
||||
FileTime fixedTime = FileTime.fromMillis(10_000L);
|
||||
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot first = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
|
||||
Files.writeString(override.toPath(), secondContent, StandardCharsets.UTF_8);
|
||||
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot second = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
|
||||
assertFalse(first.missing());
|
||||
assertFalse(second.missing());
|
||||
assertEquals(firstContent.length(), secondContent.length());
|
||||
assertEquals(first.file(), second.file());
|
||||
assertEquals(firstContent, first.content());
|
||||
assertEquals(secondContent, second.content());
|
||||
assertNotEquals(first.sha256(), second.sha256());
|
||||
assertNotEquals(first, second);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.localization.LocaleOverlay;
|
||||
import art.arcane.volmlib.util.localization.LocalizationValidationResult;
|
||||
@@ -35,6 +36,7 @@ import java.util.stream.Stream;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisLanguageTest {
|
||||
@@ -237,6 +239,26 @@ public class IrisLanguageTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedCandidateLocaleLeavesPreviousSettingsAndLanguageActive() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings live = new IrisSettings();
|
||||
live.getGeneral().setLanguage("en_US");
|
||||
IrisSettings.settings = live;
|
||||
try {
|
||||
boolean applied = IrisSettings.applyHotloadSnapshot(
|
||||
"{\"general\":{\"language\":\"../invalid\"}}",
|
||||
IrisLanguage::reload
|
||||
);
|
||||
|
||||
assertFalse(applied);
|
||||
assertSame(live, IrisSettings.settings);
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void untrustedArgumentsCannotInjectLegacyOrMiniMessageFormatting() throws Exception {
|
||||
writeOverride("de_DE", """
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class LocaleHotloadGateTest {
|
||||
@Test
|
||||
public void anchorsCooldownAtCompletedApplication() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot initial = snapshot("initial");
|
||||
LocaleHotloadSnapshot first = snapshot("first");
|
||||
LocaleHotloadSnapshot second = snapshot("second");
|
||||
gate.reset(initial);
|
||||
|
||||
assertNull(gate.observe(first, 0L));
|
||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
||||
assertNotNull(firstAttempt);
|
||||
gate.complete(firstAttempt, 500L, true);
|
||||
|
||||
assertNull(gate.observe(second, 600L));
|
||||
assertNull(gate.observe(second, 700L));
|
||||
assertNull(gate.observe(second, 3_499L));
|
||||
LocaleHotloadGate.Attempt secondAttempt = gate.observe(second, 3_500L);
|
||||
|
||||
assertNotNull(secondAttempt);
|
||||
assertEquals(second, secondAttempt.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coalescesCooldownBurstToLatestStableSnapshot() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot first = snapshot("first");
|
||||
LocaleHotloadSnapshot intermediate = snapshot("intermediate");
|
||||
LocaleHotloadSnapshot latest = snapshot("latest");
|
||||
gate.reset(snapshot("initial"));
|
||||
|
||||
assertNull(gate.observe(first, 0L));
|
||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
||||
assertNotNull(firstAttempt);
|
||||
gate.complete(firstAttempt, 200L, true);
|
||||
|
||||
assertNull(gate.observe(intermediate, 250L));
|
||||
assertNull(gate.observe(intermediate, 350L));
|
||||
assertNull(gate.observe(latest, 400L));
|
||||
assertNull(gate.observe(latest, 500L));
|
||||
assertNull(gate.observe(latest, 3_199L));
|
||||
LocaleHotloadGate.Attempt latestAttempt = gate.observe(latest, 3_200L);
|
||||
|
||||
assertNotNull(latestAttempt);
|
||||
assertEquals(latest, latestAttempt.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requiresDeletionGraceAndCancelsTransientMissingSnapshot() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot initial = snapshot("initial");
|
||||
LocaleHotloadSnapshot replacement = snapshot("replacement");
|
||||
LocaleHotloadSnapshot missing = LocaleHotloadSnapshot.missing(initial.file(), "en_US");
|
||||
gate.reset(initial);
|
||||
|
||||
assertNull(gate.observe(missing, 0L));
|
||||
assertNull(gate.observe(missing, 499L));
|
||||
assertNull(gate.observe(replacement, 500L));
|
||||
LocaleHotloadGate.Attempt replacementAttempt = gate.observe(replacement, 600L);
|
||||
assertNotNull(replacementAttempt);
|
||||
assertEquals(replacement, replacementAttempt.snapshot());
|
||||
gate.complete(replacementAttempt, 600L, true);
|
||||
|
||||
assertNull(gate.observe(missing, 700L));
|
||||
assertNull(gate.observe(missing, 1_199L));
|
||||
assertNull(gate.observe(missing, 1_200L));
|
||||
LocaleHotloadGate.Attempt deletionAttempt = gate.observe(missing, 3_600L);
|
||||
|
||||
assertNotNull(deletionAttempt);
|
||||
assertEquals(missing, deletionAttempt.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableReadBlocksStalePendingSnapshot() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot first = snapshot("first");
|
||||
LocaleHotloadSnapshot pending = snapshot("pending");
|
||||
gate.reset(snapshot("initial"));
|
||||
|
||||
assertNull(gate.observe(first, 0L));
|
||||
LocaleHotloadGate.Attempt firstAttempt = gate.observe(first, 100L);
|
||||
assertNotNull(firstAttempt);
|
||||
gate.complete(firstAttempt, 200L, true);
|
||||
assertNull(gate.observe(pending, 300L));
|
||||
assertNull(gate.observe(pending, 400L));
|
||||
|
||||
gate.unavailable();
|
||||
assertNull(gate.observe(pending, 3_200L));
|
||||
LocaleHotloadGate.Attempt recoveredAttempt = gate.observe(pending, 3_300L);
|
||||
|
||||
assertNotNull(recoveredAttempt);
|
||||
assertEquals(pending, recoveredAttempt.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedApplicationRetriesOnlyAfterCooldown() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot changed = snapshot("changed");
|
||||
gate.reset(snapshot("initial"));
|
||||
|
||||
assertNull(gate.observe(changed, 0L));
|
||||
LocaleHotloadGate.Attempt failedAttempt = gate.observe(changed, 100L);
|
||||
assertNotNull(failedAttempt);
|
||||
gate.complete(failedAttempt, 500L, false);
|
||||
|
||||
assertNull(gate.observe(changed, 3_499L));
|
||||
LocaleHotloadGate.Attempt retryAttempt = gate.observe(changed, 3_500L);
|
||||
|
||||
assertNotNull(retryAttempt);
|
||||
assertEquals(changed, retryAttempt.snapshot());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualResetInvalidatesInFlightAutomaticAttempt() {
|
||||
LocaleHotloadGate gate = gate();
|
||||
LocaleHotloadSnapshot automatic = snapshot("automatic");
|
||||
LocaleHotloadSnapshot manual = snapshot("manual");
|
||||
LocaleHotloadSnapshot changed = snapshot("changed");
|
||||
gate.reset(snapshot("initial"));
|
||||
|
||||
assertNull(gate.observe(automatic, 0L));
|
||||
LocaleHotloadGate.Attempt staleAttempt = gate.observe(automatic, 100L);
|
||||
assertNotNull(staleAttempt);
|
||||
gate.reset(manual);
|
||||
gate.complete(staleAttempt, 200L, true);
|
||||
|
||||
assertNull(gate.observe(manual, 200L));
|
||||
assertNull(gate.observe(changed, 300L));
|
||||
LocaleHotloadGate.Attempt currentAttempt = gate.observe(changed, 400L);
|
||||
|
||||
assertNotNull(currentAttempt);
|
||||
assertNotEquals(staleAttempt.generation(), currentAttempt.generation());
|
||||
assertEquals(changed, currentAttempt.snapshot());
|
||||
}
|
||||
|
||||
private LocaleHotloadGate gate() {
|
||||
return new LocaleHotloadGate(new LocaleHotloadGate.Timing(100L, 500L, 3_000L));
|
||||
}
|
||||
|
||||
private LocaleHotloadSnapshot snapshot(String content) {
|
||||
return LocaleHotloadSnapshot.present(
|
||||
new File("locale.json"),
|
||||
"en_US",
|
||||
content,
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class StudioSVCPackAdmissionTest {
|
||||
@Test
|
||||
public void loadablePackIsAdmitted() {
|
||||
PackValidationResult validation = new PackValidationResult(
|
||||
"overworld", List.of(), List.of(), 1L);
|
||||
|
||||
assertNull(StudioSVC.resolvePackAdmissionFailure(
|
||||
"overworld", Optional.empty(), validation));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingValidationFailsClosedWithTheResolvedPackName() {
|
||||
BrokenPackException failure = StudioSVC.resolvePackAdmissionFailure(
|
||||
"overworld-pack", Optional.empty(), null);
|
||||
|
||||
assertEquals("overworld-pack", failure.getPackName());
|
||||
assertEquals(List.of(
|
||||
"Required pack validation has not completed. Studio creation fails closed until validation succeeds."),
|
||||
failure.getReasons());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockingValidationPreservesEveryReasonInOrder() {
|
||||
List<String> reasons = List.of(
|
||||
"Biome 'broken' has no resolvable regions.",
|
||||
"Structure 'castle' references missing pool 'castle/start'.");
|
||||
PackValidationResult validation = new PackValidationResult(
|
||||
"overworld", reasons, List.of(), 1L);
|
||||
|
||||
BrokenPackException failure = StudioSVC.resolvePackAdmissionFailure(
|
||||
"overworld", Optional.empty(), validation);
|
||||
|
||||
assertEquals("overworld", failure.getPackName());
|
||||
assertEquals(reasons, failure.getReasons());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupDenialTakesPriorityOverCachedPackValidation() {
|
||||
String denial = "Restart the server after changing external datapacks.";
|
||||
PackValidationResult validation = new PackValidationResult(
|
||||
"overworld", List.of(), List.of(), 1L);
|
||||
|
||||
BrokenPackException failure = StudioSVC.resolvePackAdmissionFailure(
|
||||
"overworld", Optional.of(denial), validation);
|
||||
|
||||
assertEquals(List.of(denial), failure.getReasons());
|
||||
}
|
||||
}
|
||||
@@ -112,9 +112,9 @@ public class VillageImporterBundleTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeMaximumDistanceControlsIrisAssemblyRadius() {
|
||||
public void registeredJigsawMetadataIncludesAncientCitySourceAndAssemblyContract() {
|
||||
Map<String, Object> structure = VillageImporter.structureJson(
|
||||
"minecraft:village_plains",
|
||||
"minecraft:ancient_city",
|
||||
"village/pool/minecraft/start",
|
||||
6,
|
||||
81
|
||||
@@ -122,8 +122,10 @@ public class VillageImporterBundleTest {
|
||||
|
||||
assertEquals(6, structure.get("maxDepth"));
|
||||
assertEquals(6, structure.get("maxSizeChunks"));
|
||||
assertEquals("STRUCTURE_PIECE", structure.get("placeMode"));
|
||||
assertEquals(IrisJigsawBranchFailurePolicy.TERMINATE_BRANCH.name(),
|
||||
structure.get("branchFailurePolicy"));
|
||||
assertEquals("minecraft:ancient_city", structure.get("vanillaSource"));
|
||||
}
|
||||
|
||||
private StructureResourceBundle bundle(String objectContent) {
|
||||
|
||||
+1
-1
@@ -30,4 +30,4 @@ apiVersion=26.1
|
||||
fabricLoaderVersion=0.19.3
|
||||
forgeVersion=26.2-65.1.1
|
||||
neoForgeVersion=26.2.0.59
|
||||
volmLibCoordinate=com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522
|
||||
volmLibCoordinate=com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ application {
|
||||
}
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
|
||||
Reference in New Issue
Block a user