mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Object placement golden test
- Record 4000 placements (200 objects x 4 rotations x 5 seeds) as one block-stream hash with RNG draw state
This commit is contained in:
1 parent
f283d482a4
commit
b526b480a7
1 file changed
+707
@@ -0,0 +1,707 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.BlockDataMergeSupport;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.cave.CarvingMode;
|
||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.validation.ContentGate;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
public class ObjectPlacementGoldenTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int OBJECTS = 200;
|
||||
private static final int SEEDS = 5;
|
||||
private static final long GOLDEN = 0x408f9d4113015897L;
|
||||
|
||||
private static final List<String> PALETTE = List.of(
|
||||
"minecraft:stone",
|
||||
"minecraft:dirt",
|
||||
"minecraft:grass_block[snowy=false]",
|
||||
"minecraft:oak_log[axis=y]",
|
||||
"minecraft:oak_log[axis=x]",
|
||||
"minecraft:oak_leaves[distance=3,persistent=false,waterlogged=false]",
|
||||
"minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]",
|
||||
"minecraft:oak_leaves[distance=7,persistent=false,waterlogged=true]",
|
||||
"minecraft:oak_stairs[facing=north,half=bottom,waterlogged=false]",
|
||||
"minecraft:oak_stairs[facing=east,half=top,waterlogged=true]",
|
||||
"minecraft:vine[east=false,north=true,south=false,up=false,west=false]",
|
||||
"minecraft:air",
|
||||
"minecraft:cave_air",
|
||||
"minecraft:chest[facing=south,waterlogged=false]",
|
||||
"minecraft:lantern[hanging=false,waterlogged=false]",
|
||||
"minecraft:cobblestone",
|
||||
"minecraft:glass",
|
||||
"minecraft:oak_slab[type=bottom,waterlogged=false]");
|
||||
|
||||
private final Map<String, GoldenState> states = new ConcurrentHashMap<>();
|
||||
private IrisData data;
|
||||
private IrisData objectData;
|
||||
private Engine engine;
|
||||
private IrisObjectRotation.StateRotator previousRotator;
|
||||
private BlockDataMergeSupport.StateMerger previousMerger;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
|
||||
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
|
||||
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
previousRotator = IrisObjectRotation.bindPlatformRotator(ObjectPlacementGoldenTest::rotateState);
|
||||
previousMerger = BlockDataMergeSupport.bindPlatformMerger(this::mergeStates);
|
||||
|
||||
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
|
||||
when(rock.get(anyDouble(), anyDouble())).thenAnswer(invocation -> state("minecraft:stone"));
|
||||
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
|
||||
when(complex.getRockStream()).thenReturn(rock);
|
||||
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
|
||||
engine = mock(Engine.class, withSettings().stubOnly());
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
ContentGate gate = mock(ContentGate.class, withSettings().stubOnly());
|
||||
when(gate.ready()).thenReturn(false);
|
||||
data = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
when(data.getContentGate()).thenReturn(gate);
|
||||
objectData = mock(IrisData.class, withSettings().stubOnly());
|
||||
when(objectData.getContentGate()).thenReturn(gate);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisObjectRotation.restorePlatformRotator(previousRotator);
|
||||
BlockDataMergeSupport.restorePlatformMerger(previousMerger);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementsMatchTheRecordedBlockStream() throws IOException {
|
||||
Random random = new Random(20261021L);
|
||||
List<IrisObjectRotation> rotations = rotations();
|
||||
long combined = 17L;
|
||||
StringBuilder dump = new StringBuilder();
|
||||
for (int index = 0; index < OBJECTS; index++) {
|
||||
IrisObject object = randomObject(random, index);
|
||||
object.setLoader(objectData);
|
||||
IrisObjectPlacement base = randomPlacement(random);
|
||||
for (int rotationIndex = 0; rotationIndex < rotations.size(); rotationIndex++) {
|
||||
IrisObjectPlacement placement = base.toPlacement("golden");
|
||||
placement.setRotation(rotations.get(rotationIndex));
|
||||
boolean explicitY = (index + rotationIndex) % 3 == 0;
|
||||
for (int seed = 0; seed < SEEDS; seed++) {
|
||||
RNG rng = new RNG(index * 1_000_003L + rotationIndex * 7919L + seed);
|
||||
GoldenPlacer placer = new GoldenPlacer(engine, (index & 1) == 0);
|
||||
List<String> listened = new ArrayList<>();
|
||||
int x = 400 + index * 3 + seed;
|
||||
int z = -900 + rotationIndex * 5 - seed;
|
||||
int y = explicitY ? 90 + seed : -1;
|
||||
int result;
|
||||
if (placement.getMode() == ObjectPlaceMode.ORGANIC_STILT && explicitY && seed % 2 == 0) {
|
||||
result = object.placeOnFloor(x, 70, z, placer, placement, rng,
|
||||
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
|
||||
data);
|
||||
} else {
|
||||
result = object.place(x, y, z, placer, placement, rng,
|
||||
(position, state) -> listened.add(position.getX() + "," + position.getY() + "," + position.getZ() + "=" + state.key()),
|
||||
null, data);
|
||||
}
|
||||
long hash = placer.hash;
|
||||
hash = mix(hash, result);
|
||||
hash = mix(hash, rng.nextLong());
|
||||
for (String entry : listened) {
|
||||
hash = mix(hash, entry.hashCode());
|
||||
}
|
||||
hash = mix(hash, listened.size());
|
||||
combined = mix(combined, hash);
|
||||
dump.append(index).append(' ').append(rotationIndex).append(' ').append(seed).append(' ')
|
||||
.append(Long.toHexString(hash)).append(' ').append(placer.writes).append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
String dumpPath = System.getenv("IRIS_OBJECT_GOLDEN_DUMP");
|
||||
if (dumpPath != null && !dumpPath.isBlank()) {
|
||||
Files.writeString(Path.of(dumpPath), dump.toString());
|
||||
}
|
||||
assertEquals(Long.toHexString(GOLDEN), Long.toHexString(combined));
|
||||
}
|
||||
|
||||
private static long mix(long hash, long value) {
|
||||
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
|
||||
return mixed ^ (mixed >>> 29);
|
||||
}
|
||||
|
||||
private List<IrisObjectRotation> rotations() {
|
||||
IrisObjectRotation defaultY = new IrisObjectRotation();
|
||||
IrisObjectRotation lockedQuarter = IrisObjectRotation.of(0D, 90D, 0D);
|
||||
IrisObjectRotation flipped = rotation(new IrisAxisRotationClamp(true, true, 180D, 180D, 0D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp());
|
||||
IrisObjectRotation angled = rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
|
||||
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
|
||||
return List.of(defaultY, lockedQuarter, flipped, angled);
|
||||
}
|
||||
|
||||
private IrisObjectPlacement randomPlacement(Random random) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.CENTER_HEIGHT,
|
||||
ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
|
||||
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT,
|
||||
ObjectPlaceMode.CEILING_HANG, ObjectPlaceMode.MAX_HEIGHT, ObjectPlaceMode.FAST_MAX_HEIGHT,
|
||||
ObjectPlaceMode.MIN_HEIGHT, ObjectPlaceMode.PAINT, ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_ORGANIC,
|
||||
ObjectPlaceMode.STRUCTURE_PIECE, ObjectPlaceMode.FLOATING, ObjectPlaceMode.FAST_MIN_STILT);
|
||||
placement.setMode(modes.get(random.nextInt(modes.size())));
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setTranslate(new IrisObjectTranslate()
|
||||
.setX(random.nextInt(7) - 3)
|
||||
.setY(random.nextInt(5) - 2)
|
||||
.setZ(random.nextInt(7) - 3)
|
||||
.setYRandom(random.nextInt(3) - 1));
|
||||
}
|
||||
if (random.nextInt(6) == 0) {
|
||||
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(4)));
|
||||
}
|
||||
if (random.nextInt(3) == 0) {
|
||||
placement.setEdit(randomEdits(random));
|
||||
}
|
||||
if (random.nextInt(4) == 0) {
|
||||
placement.setStiltSettings(new IrisStiltSettings());
|
||||
}
|
||||
if (random.nextInt(5) == 0) {
|
||||
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(4)));
|
||||
}
|
||||
placement.setWaterloggable(random.nextInt(4) == 0);
|
||||
placement.setUnderwater(random.nextInt(8) == 0);
|
||||
placement.setOnwater(random.nextInt(10) == 0);
|
||||
placement.setMeld(random.nextInt(8) == 0);
|
||||
placement.setBore(random.nextInt(10) == 0);
|
||||
placement.setForcePlace(random.nextInt(3) == 0);
|
||||
placement.setBottom(random.nextInt(6) == 0);
|
||||
placement.setSnow(random.nextInt(5) == 0 ? 0.7D : 0D);
|
||||
placement.setRequireSurfaceSupport(random.nextInt(3) != 0);
|
||||
placement.setSurfaceSupportBuffer(random.nextInt(3));
|
||||
placement.setSurfaceSupportDepth(1 + random.nextInt(2));
|
||||
placement.setCarvingSupport(switch (random.nextInt(5)) {
|
||||
case 0 -> CarvingMode.ANYWHERE;
|
||||
case 1 -> CarvingMode.CARVING_ONLY;
|
||||
default -> CarvingMode.SURFACE_ONLY;
|
||||
});
|
||||
return placement;
|
||||
}
|
||||
|
||||
private KList<IrisObjectReplace> randomEdits(Random random) {
|
||||
KList<IrisObjectReplace> edits = new KList<>();
|
||||
int count = 1 + random.nextInt(3);
|
||||
for (int index = 0; index < count; index++) {
|
||||
IrisObjectReplace replace = new IrisObjectReplace();
|
||||
switch (random.nextInt(4)) {
|
||||
case 0 -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_leaves")));
|
||||
replace.setReplace(palette("minecraft:cobblestone"));
|
||||
}
|
||||
case 1 -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:oak_stairs")));
|
||||
replace.setReplace(palette("minecraft:oak_stairs[half=top]"));
|
||||
}
|
||||
case 2 -> {
|
||||
replace.setExact(true);
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:stone")));
|
||||
replace.setReplace(palette("minecraft:glass"));
|
||||
}
|
||||
default -> {
|
||||
replace.setFind(new KList<>(new IrisBlockData("minecraft:grass_block"), new IrisBlockData("minecraft:dirt")));
|
||||
replace.setReplace(palette("minecraft:oak_leaves[distance=2,persistent=false,waterlogged=false]"));
|
||||
}
|
||||
}
|
||||
replace.setChance(switch (random.nextInt(3)) {
|
||||
case 0 -> 1F;
|
||||
case 1 -> 0.5F;
|
||||
default -> 0.15F;
|
||||
});
|
||||
edits.add(replace);
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
private static IrisMaterialPalette palette(String key) {
|
||||
IrisMaterialPalette palette = new IrisMaterialPalette();
|
||||
palette.setPalette(new KList<>(new IrisBlockData(key)));
|
||||
return palette;
|
||||
}
|
||||
|
||||
private IrisObject randomObject(Random random, int index) {
|
||||
int width = 1 + random.nextInt(9);
|
||||
int height = 1 + random.nextInt(9);
|
||||
int depth = 1 + random.nextInt(9);
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 2));
|
||||
List<IrisBlockVector> placed = new ArrayList<>();
|
||||
for (int block = 0; block < count; block++) {
|
||||
int x = random.nextInt(width) - (width / 2);
|
||||
int y = random.nextInt(height) - (height / 2);
|
||||
int z = random.nextInt(depth) - (depth / 2);
|
||||
IrisBlockVector position = new IrisBlockVector(x, y, z);
|
||||
String key = PALETTE.get(random.nextInt(PALETTE.size()));
|
||||
object.getBlocks().put(position, state(key));
|
||||
placed.add(position);
|
||||
if (key.startsWith("minecraft:chest") && random.nextBoolean()) {
|
||||
object.getStates().put(position, new TileData("minecraft:chest", new KMap<>()));
|
||||
}
|
||||
}
|
||||
if (index % 4 == 1 && !placed.isEmpty()) {
|
||||
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>();
|
||||
for (IrisBlockVector position : placed) {
|
||||
if (random.nextInt(3) == 0) {
|
||||
canopy.add(new TreeBlockCanvas.Vec(position.getBlockX(), position.getBlockY(), position.getBlockZ()));
|
||||
}
|
||||
}
|
||||
IrisBlockVector root = placed.get(0);
|
||||
roots.add(new TreeGrounding.Root(root.getBlockX(), -(height / 2), root.getBlockZ(), state("minecraft:oak_log[axis=y]")));
|
||||
object.setTreeGrounding(TreeGrounding.create(roots, 4, canopy));
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
|
||||
IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
rotation.setEnabled(true);
|
||||
rotation.setXAxis(x);
|
||||
rotation.setYAxis(y);
|
||||
rotation.setZAxis(z);
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private GoldenState state(String raw) {
|
||||
String key = raw.toLowerCase();
|
||||
if (key.indexOf(':') < 0) {
|
||||
key = "minecraft:" + key;
|
||||
}
|
||||
return states.computeIfAbsent(key, GoldenState::new);
|
||||
}
|
||||
|
||||
private NativeBlockState mergeStates(NativeBlockState base, NativeBlockState update) {
|
||||
GoldenState left = (GoldenState) base;
|
||||
GoldenState right = (GoldenState) update;
|
||||
if (!left.material.equals(right.material)) {
|
||||
return update;
|
||||
}
|
||||
LinkedHashMap<String, String> merged = new LinkedHashMap<>(left.properties);
|
||||
merged.putAll(right.properties);
|
||||
return state(GoldenState.compose(left.material, merged));
|
||||
}
|
||||
|
||||
private static NativeBlockState rotateState(IrisObjectRotation rotation, NativeBlockState state, int spinX, int spinY, int spinZ) {
|
||||
if (state == null || !rotation.canRotate()) {
|
||||
return state;
|
||||
}
|
||||
GoldenState golden = (GoldenState) state;
|
||||
String facing = golden.properties.get("facing");
|
||||
if (facing != null) {
|
||||
IrisBlockVector vector = switch (facing) {
|
||||
case "north" -> new IrisBlockVector(0, 0, -1);
|
||||
case "south" -> new IrisBlockVector(0, 0, 1);
|
||||
case "east" -> new IrisBlockVector(1, 0, 0);
|
||||
case "west" -> new IrisBlockVector(-1, 0, 0);
|
||||
case "up" -> new IrisBlockVector(0, 1, 0);
|
||||
default -> new IrisBlockVector(0, -1, 0);
|
||||
};
|
||||
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
|
||||
String face = faceName(rotated);
|
||||
if (face.equals("up") || face.equals("down")) {
|
||||
return golden.material.endsWith("_stairs") ? null : state;
|
||||
}
|
||||
return face.equals(facing) ? state : golden.with("facing", face);
|
||||
}
|
||||
String axis = golden.properties.get("axis");
|
||||
if (axis != null) {
|
||||
IrisBlockVector vector = switch (axis) {
|
||||
case "x" -> new IrisBlockVector(1, 0, 0);
|
||||
case "y" -> new IrisBlockVector(0, 1, 0);
|
||||
default -> new IrisBlockVector(0, 0, 1);
|
||||
};
|
||||
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
|
||||
double ax = Math.abs(rotated.getX());
|
||||
double ay = Math.abs(rotated.getY());
|
||||
double az = Math.abs(rotated.getZ());
|
||||
String result = ax >= ay && ax >= az ? "x" : ay >= az ? "y" : "z";
|
||||
return result.equals(axis) ? state : golden.with("axis", result);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static String faceName(IrisBlockVector vector) {
|
||||
double ax = Math.abs(vector.getX());
|
||||
double ay = Math.abs(vector.getY());
|
||||
double az = Math.abs(vector.getZ());
|
||||
if (ay >= ax && ay >= az) {
|
||||
return vector.getY() > 0 ? "up" : "down";
|
||||
}
|
||||
if (ax >= az) {
|
||||
return vector.getX() > 0 ? "east" : "west";
|
||||
}
|
||||
return vector.getZ() > 0 ? "south" : "north";
|
||||
}
|
||||
|
||||
private final class GoldenState implements NativeBlockState {
|
||||
private final String key;
|
||||
private final String material;
|
||||
private final LinkedHashMap<String, String> properties;
|
||||
|
||||
private GoldenState(String key) {
|
||||
this.key = key;
|
||||
int bracket = key.indexOf('[');
|
||||
this.material = bracket < 0 ? key : key.substring(0, bracket);
|
||||
this.properties = new LinkedHashMap<>();
|
||||
if (bracket >= 0) {
|
||||
for (String part : key.substring(bracket + 1, key.length() - 1).split(",")) {
|
||||
int split = part.indexOf('=');
|
||||
properties.put(part.substring(0, split), part.substring(split + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String compose(String material, Map<String, String> properties) {
|
||||
if (properties.isEmpty()) {
|
||||
return material;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(material).append('[');
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : properties.entrySet()) {
|
||||
if (!first) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(entry.getKey()).append('=').append(entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
return builder.append(']').toString();
|
||||
}
|
||||
|
||||
private GoldenState with(String name, String value) {
|
||||
LinkedHashMap<String, String> updated = new LinkedHashMap<>(properties);
|
||||
updated.put(name, value);
|
||||
return state(compose(material, updated));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return "minecraft";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
return material.endsWith("air");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
return !isAir() && !isFluid() && !isVineBlock() && !material.equals("minecraft:lantern");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOccluding() {
|
||||
return material.equals("minecraft:stone") || material.equals("minecraft:dirt")
|
||||
|| material.equals("minecraft:grass_block") || material.equals("minecraft:oak_log")
|
||||
|| material.equals("minecraft:cobblestone") || material.equals("minecraft:oak_slab");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
return material.equals("minecraft:water");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWater() {
|
||||
return isFluid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWaterLogged() {
|
||||
return "true".equals(properties.get("waterlogged"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdatable() {
|
||||
return material.endsWith("_stairs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
return material.endsWith("_log") || material.endsWith("_leaves");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return material.equals("minecraft:grass_block") || material.equals("minecraft:dirt");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecorant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorage() {
|
||||
return material.equals("minecraft:chest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return isStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOre() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeepSlate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVineBlock() {
|
||||
return material.equals("minecraft:vine");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlaceOnto(NativeBlockState onto) {
|
||||
return onto != null && onto.isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(NativeBlockState state) {
|
||||
if (!(state instanceof GoldenState other) || !other.material.equals(material)) {
|
||||
return false;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : other.properties.entrySet()) {
|
||||
if (!entry.getValue().equals(properties.get(entry.getKey()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTileEntity() {
|
||||
return isStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState withProperty(String name, String value) {
|
||||
return with(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private final class GoldenPlacer implements IObjectPlacer {
|
||||
private final Engine engine;
|
||||
private final boolean preventDecay;
|
||||
private final Map<Long, NativeBlockState> blocks = new HashMap<>();
|
||||
private final Map<Long, Map<Class<?>, Object>> extra = new HashMap<>();
|
||||
private long hash = 31L;
|
||||
private int writes;
|
||||
|
||||
private GoldenPlacer(Engine engine, boolean preventDecay) {
|
||||
this.engine = engine;
|
||||
this.preventDecay = preventDecay;
|
||||
}
|
||||
|
||||
private static long key(int x, int y, int z) {
|
||||
return (((long) x & 0x3FFFFFL) << 42) | (((long) y & 0xFFFFFL) << 22) | ((long) z & 0x3FFFFFL);
|
||||
}
|
||||
|
||||
private int surface(int x, int z) {
|
||||
return 64 + Math.floorMod(x * 31 + z * 17, 9) - 4;
|
||||
}
|
||||
|
||||
private void record(String kind, int x, int y, int z, String value) {
|
||||
writes++;
|
||||
hash = mix(hash, kind.hashCode());
|
||||
hash = mix(hash, x);
|
||||
hash = mix(hash, y);
|
||||
hash = mix(hash, z);
|
||||
hash = mix(hash, value.hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return surface(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return ignoreFluid ? surface(x, z) : Math.max(surface(x, z), 62);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, NativeBlockState d) {
|
||||
record("set", x, y, z, d == null ? "null" : d.key());
|
||||
blocks.put(key(x, y, z), d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState written = blocks.get(key(x, y, z));
|
||||
if (written != null) {
|
||||
return written;
|
||||
}
|
||||
int surface = surface(x, z);
|
||||
if (y <= surface) {
|
||||
return state(y == surface ? "minecraft:grass_block[snowy=false]" : "minecraft:stone");
|
||||
}
|
||||
if (y <= 62) {
|
||||
return state("minecraft:water");
|
||||
}
|
||||
return Math.floorMod(x + y + z, 13) == 0 ? state("minecraft:oak_leaves[distance=1,persistent=true,waterlogged=true]") : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return preventDecay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return Math.floorMod(x * 7 + y * 3 + z * 11, 29) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
NativeBlockState state = get(x, y, z);
|
||||
return state != null && state.isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return surface(x, z) < 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight(int x, int z) {
|
||||
return 62;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
record("tile", xx, yy, zz, tile == null ? "null" : "tile");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T value) {
|
||||
record("data", xx, yy, zz, value == null ? "null" : value.getClass().getSimpleName() + ":" + value);
|
||||
extra.computeIfAbsent(key(xx, yy, zz), ignored -> new HashMap<>()).put(value == null ? Object.class : value.getClass(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> type) {
|
||||
Map<Class<?>, Object> values = extra.get(key(xx, yy, zz));
|
||||
Object value = values == null ? null : values.get(type);
|
||||
return value == null ? null : type.cast(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
boolean open = IObjectPlacer.super.beginTreeCanopy(position);
|
||||
record("canopy", position.getX(), position.getY(), position.getZ(), Boolean.toString(open));
|
||||
return open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user