Add bluemap:missing block and basic unit tests.

The bluemap:missing block will be used as fallback if bluemap cant render another block for some reason.
The unit tests are just the setup, they will need to catch up over time ^^
This commit is contained in:
Blue (Lukas Rieger) 2020-01-05 18:52:30 +01:00
parent d20e0843ab
commit bfdf7fa7ce
18 changed files with 220 additions and 38 deletions

View File

@ -11,6 +11,8 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Test with Gradle
run: ./gradlew test
- name: Build with Gradle
run: ./gradlew shadowJar
- uses: actions/upload-artifact@v1

View File

@ -90,7 +90,8 @@ public void renderMaps() throws IOException {
config.getWebDataPath().toFile().mkdirs();
Map<String, MapType> maps = new HashMap<>();
configManager.getBlockPropertiesConfig().setResourcePack(resourcePack);
for (MapConfig mapConfig : config.getMapConfigs()) {
File mapPath = new File(mapConfig.getWorldPath());
if (!mapPath.exists() || !mapPath.isDirectory()) {

View File

@ -134,7 +134,9 @@ public void render() {
long ert = (long)((time / pct) * (1d - pct));
String ertDurationString = DurationFormatUtils.formatDurationWords(ert, true, true);
Logger.global.logInfo("Rendered " + renderedTiles + " of " + tileCount + " tiles in " + durationString);
double tps = renderedTiles / (time / 1000.0);
Logger.global.logInfo("Rendered " + renderedTiles + " of " + tileCount + " tiles in " + durationString + " | " + GenericMath.round(tps, 3) + " tiles/s");
Logger.global.logInfo(GenericMath.round(pct * 100, 3) + "% | Estimated remaining time: " + ertDurationString);
}

View File

@ -8,6 +8,8 @@ dependencies {
compile 'ninja.leaping.configurate:configurate-gson:3.3'
compile 'ninja.leaping.configurate:configurate-yaml:3.3'
compile 'com.github.Querz:NBT:4.0'
testCompile 'junit:junit:4.12'
}
task zipWebroot(type: Zip) {

View File

@ -84,7 +84,7 @@ public BlockState get(int id, int meta) {
BlockState state = mappings.get(idmeta);
if (state == null) {
state = mappings.getOrDefault(new BlockIDMeta(id, 0), BlockState.AIR); //meta-fallback
state = mappings.getOrDefault(new BlockIDMeta(id, 0), BlockState.MISSING); //meta-fallback
if (autopoulationConfigLoader != null) {
mappings.put(idmeta, state);

View File

@ -31,11 +31,14 @@
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.mapping.BlockPropertiesMapper;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resourcepack.TransformedBlockModelResource;
import de.bluecolored.bluemap.core.world.BlockProperties;
import de.bluecolored.bluemap.core.world.BlockState;
import ninja.leaping.configurate.ConfigurationNode;
@ -48,6 +51,8 @@ public class BlockPropertiesConfig implements BlockPropertiesMapper {
private Multimap<String, BlockStateMapping<BlockProperties>> mappings;
private LoadingCache<BlockState, BlockProperties> mappingCache;
private ResourcePack resourcePack = null;
public BlockPropertiesConfig(ConfigurationNode node) throws IOException {
this(node, null);
}
@ -55,7 +60,7 @@ public BlockPropertiesConfig(ConfigurationNode node) throws IOException {
public BlockPropertiesConfig(ConfigurationNode node, ConfigurationLoader<? extends ConfigurationNode> autopoulationConfigLoader) throws IOException {
this.autopoulationConfigLoader = autopoulationConfigLoader;
mappings = HashMultimap.create();
mappings = MultimapBuilder.hashKeys().arrayListValues().build();
for (Entry<Object, ? extends ConfigurationNode> e : node.getChildrenMap().entrySet()){
String key = e.getKey().toString();
@ -81,6 +86,14 @@ public BlockPropertiesConfig(ConfigurationNode node, ConfigurationLoader<? exten
});
}
/**
* Sets the {@link ResourcePack} of this PropertyMapper, so it can generate better defaults if the mapping is missing
* @param resourcePack the {@link ResourcePack}
*/
public void setResourcePack(ResourcePack resourcePack) {
this.resourcePack = resourcePack;
}
@Override
public BlockProperties get(BlockState from){
try {
@ -98,16 +111,32 @@ private BlockProperties mapNoCache(BlockState bs){
}
}
BlockProperties generated = BlockProperties.DEFAULT;
if (resourcePack != null) {
try {
boolean culling = false;
boolean occluding = false;
for(TransformedBlockModelResource model : resourcePack.getBlockStateResource(bs).getModels(bs)) {
culling = culling || model.getModel().isCulling();
occluding = occluding || model.getModel().isOccluding();
if (culling && occluding) break;
}
generated = new BlockProperties(culling, occluding, generated.isFlammable());
} catch (NoSuchResourceException ignore) {} //ignoring this because it will be logged later again if we try to render that block
}
mappings.put(bs.getFullId(), new BlockStateMapping<BlockProperties>(new BlockState(bs.getFullId()), generated));
if (autopoulationConfigLoader != null) {
mappings.put(bs.getFullId(), new BlockStateMapping<BlockProperties>(new BlockState(bs.getFullId()), BlockProperties.DEFAULT));
synchronized (autopoulationConfigLoader) {
try {
ConfigurationNode node = autopoulationConfigLoader.load();
ConfigurationNode bpNode = node.getNode(bs.getFullId());
bpNode.getNode("culling").setValue(false);
bpNode.getNode("occluding").setValue(false);
bpNode.getNode("flammable").setValue(false);
bpNode.getNode("culling").setValue(generated.isCulling());
bpNode.getNode("occluding").setValue(generated.isOccluding());
bpNode.getNode("flammable").setValue(generated.isFlammable());
autopoulationConfigLoader.save(node);
} catch (IOException ex) {
Logger.global.noFloodError("blockpropsconf-autopopulate-ioex", "Failed to auto-populate BlockPropertiesConfig!", ex);
@ -115,7 +144,7 @@ private BlockProperties mapNoCache(BlockState bs){
}
}
return BlockProperties.DEFAULT;
return generated;
}
}

View File

@ -188,7 +188,7 @@ public BlockState getBlockState(Vector3i pos) {
if (value >= palette.length) {
Logger.global.noFloodWarning("palettewarning", "Got palette value " + value + " but palette has size of " + palette.length + " (Future occasions of this error will not be logged)");
return BlockState.AIR;
return BlockState.MISSING;
}
return palette[(int) value];

View File

@ -139,7 +139,7 @@ public BlockState getBlockState(Vector3i pos) {
return chunk.getBlockState(pos);
} catch (Exception ex) {
return BlockState.AIR;
return BlockState.MISSING;
}
}

View File

@ -31,6 +31,7 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.render.RenderSettings;
import de.bluecolored.bluemap.core.render.WorldTile;
import de.bluecolored.bluemap.core.render.context.ExtendedBlockContext;
import de.bluecolored.bluemap.core.render.context.SlicedWorldChunkBlockContext;
import de.bluecolored.bluemap.core.render.hires.blockmodel.BlockStateModel;
import de.bluecolored.bluemap.core.render.hires.blockmodel.BlockStateModelFactory;
@ -39,6 +40,7 @@
import de.bluecolored.bluemap.core.util.AABB;
import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.ChunkNotGeneratedException;
import de.bluecolored.bluemap.core.world.WorldChunk;
@ -79,11 +81,19 @@ public HiresModel render(WorldTile tile, AABB region, RenderSettings renderSetti
maxHeight = y;
ExtendedBlockContext context = new SlicedWorldChunkBlockContext(chunk, new Vector3i(x, y, z), renderSettings.getSliceY());
BlockStateModel blockModel;
try {
blockModel = modelFactory.createFrom(block.getBlock(), new SlicedWorldChunkBlockContext(chunk, new Vector3i(x, y, z), renderSettings.getSliceY()), renderSettings);
blockModel = modelFactory.createFrom(block.getBlock(), context, renderSettings);
} catch (NoSuchResourceException e) {
blockModel = new BlockStateModel();
try {
blockModel = modelFactory.createFrom(BlockState.MISSING, context, renderSettings);
} catch (NoSuchResourceException e2) {
e.addSuppressed(e2);
blockModel = new BlockStateModel();
}
Logger.global.noFloodDebug(block.getBlock().getFullId() + "-hiresModelRenderer-blockmodelerr", "Failed to create BlockModel for BlockState: " + block.getBlock() + " (" + e.toString() + ")");
}

View File

@ -54,6 +54,9 @@ public class BlockModelResource {
private ModelType modelType = ModelType.NORMAL;
private boolean culling = false;
private boolean occluding = false;
private boolean ambientOcclusion = true;
private Collection<Element> elements = new ArrayList<>();
private Map<String, Texture> textures = new HashMap<>();
@ -67,6 +70,14 @@ public ModelType getType() {
public boolean isAmbientOcclusion() {
return ambientOcclusion;
}
public boolean isCulling() {
return culling;
}
public boolean isOccluding() {
return occluding;
}
public Collection<Element> getElements() {
return elements;
@ -82,7 +93,8 @@ public class Element {
private Rotation rotation = new Rotation();
private boolean shade = true;
private EnumMap<Direction, Face> faces = new EnumMap<>(Direction.class);
private boolean fullCube = false;
private Element() {}
public Vector4f getDefaultUV(Direction face) {
@ -137,6 +149,10 @@ public Rotation getRotation() {
public boolean isShade() {
return shade;
}
public boolean isFullCube() {
return fullCube;
}
public EnumMap<Direction, Face> getFaces() {
return faces;
@ -216,6 +232,8 @@ public static Builder builder(FileAccess sourcesAccess, ResourcePack resourcePac
public static class Builder {
private static final String JSON_COMMENT = "__comment";
private static final Vector3f FULL_CUBE_FROM = Vector3f.ZERO;
private static final Vector3f FULL_CUBE_TO = new Vector3f(16f, 16f, 16f);
private FileAccess sourcesAccess;
private ResourcePack resourcePack;
@ -288,6 +306,35 @@ private BlockModelResource buildNoReset(String modelPath, boolean renderElements
}
}
//check block properties
for (Element element : blockModel.elements) {
if (element.isFullCube()) {
blockModel.occluding = true;
blockModel.culling = true;
for (Direction dir : Direction.values()) {
Face face = element.faces.get(dir);
if (face == null) {
blockModel.culling = false;
break;
}
Texture texture = face.getTexture();
if (texture == null) {
blockModel.culling = false;
break;
}
if (texture.getColor().getW() < 1) {
blockModel.culling = false;
break;
}
}
break;
}
}
return blockModel;
}
@ -297,7 +344,9 @@ private Element buildElement(BlockModelResource model, ConfigurationNode node, S
element.from = readVector3f(node.getNode("from"));
element.to = readVector3f(node.getNode("to"));
element.shade = node.getNode("shade").getBoolean(false);
element.shade = node.getNode("shade").getBoolean(true);
boolean fullElement = element.from.equals(FULL_CUBE_FROM) && element.to.equals(FULL_CUBE_TO);
if (!node.getNode("rotation").isVirtual()) {
element.rotation.angle = node.getNode("rotation", "angle").getFloat(0);
@ -306,6 +355,7 @@ private Element buildElement(BlockModelResource model, ConfigurationNode node, S
element.rotation.rescale = node.getNode("rotation", "rescale").getBoolean(false);
}
boolean allDirs = true;
for (Direction direction : Direction.values()) {
ConfigurationNode faceNode = node.getNode("faces", direction.name().toLowerCase());
if (!faceNode.isVirtual()) {
@ -315,9 +365,13 @@ private Element buildElement(BlockModelResource model, ConfigurationNode node, S
} catch (ParseResourceException | IOException ex) {
Logger.global.logDebug("Failed to parse an " + direction + " face for the model " + topModelPath + "! " + ex);
}
} else {
allDirs = false;
}
}
if (fullElement && allDirs) element.fullCube = true;
return element;
}

View File

@ -26,7 +26,7 @@
public class BlockProperties {
public static final BlockProperties DEFAULT = new BlockProperties(false, false, false);
public static final BlockProperties DEFAULT = new BlockProperties(true, true, false);
private final boolean culling, occluding, flammable;

View File

@ -42,9 +42,10 @@
*/
public class BlockState {
private static Pattern BLOCKSTATE_SERIALIZATION_PATTERN = Pattern.compile("^(.+?)(?:\\[(.+)\\])?$");
private static Pattern BLOCKSTATE_SERIALIZATION_PATTERN = Pattern.compile("^(.+?)(?:\\[(.*)\\])?$");
public static final BlockState AIR = new BlockState("minecraft:air", Collections.emptyMap());
public static final BlockState MISSING = new BlockState("bluemap:missing", Collections.emptyMap());
private boolean hashed;
private int hash;
@ -133,24 +134,6 @@ public BlockState with(String property, String value) {
return new BlockState(this, property, value);
}
public final boolean checkVariantCondition(String condition){
if (condition.isEmpty() || condition.equals("normal")) return true;
Map<String, String> blockProperties = getProperties();
String[] conditions = condition.split(",");
for (String c : conditions){
String[] kv = c.split("=", 2);
String key = kv[0];
String value = kv[1];
if (!value.equals(blockProperties.get(key))){
return false;
}
}
return true;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BlockState)) return false;
@ -187,7 +170,7 @@ public static BlockState fromString(String serializedBlockState) throws IllegalA
Map<String, String> pt = new HashMap<>();
String g2 = m.group(2);
if (g2 != null){
if (g2 != null && !g2.isEmpty()){
String[] propertyStrings = g2.trim().split(",");
for (String s : propertyStrings){
String[] kv = s.split("=", 2);

View File

@ -0,0 +1,5 @@
{
"variants": {
"": { "model": "bluemap:block/missing" }
}
}

View File

@ -0,0 +1,6 @@
{
"parent": "block/cube_all",
"textures": {
"all": "bluemap:block/missing"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,86 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class BlockStateTest {
@Test
public void testIdNamespace() {
BlockState blockState = new BlockState("someblock");
assertEquals("minecraft:someblock", blockState.getFullId());
assertEquals("minecraft", blockState.getNamespace());
assertEquals("someblock", blockState.getId());
blockState = new BlockState("somemod:someblock");
assertEquals("somemod:someblock", blockState.getFullId());
assertEquals("somemod", blockState.getNamespace());
assertEquals("someblock", blockState.getId());
}
@Test
public void testToString() {
BlockState blockState = new BlockState("someblock");
assertEquals("minecraft:someblock[]", blockState.toString());
blockState = blockState.with("testProp", "testVal");
assertEquals("minecraft:someblock[testProp=testVal]", blockState.toString());
blockState = blockState.with("testProp2", "testVal2");
String toString = blockState.toString();
assertTrue(
toString.equals("minecraft:someblock[testProp=testVal,testProp2=testVal2]") ||
toString.equals("minecraft:someblock[testProp2=testVal2,testProp=testVal]")
);
}
@Test
public void testFromString() {
BlockState blockState = BlockState.fromString("somemod:someblock");
assertEquals("somemod:someblock", blockState.getFullId());
assertEquals("somemod", blockState.getNamespace());
assertEquals("someblock", blockState.getId());
assertTrue(blockState.getProperties().isEmpty());
blockState = BlockState.fromString("somemod:someblock[]");
assertEquals("somemod:someblock", blockState.getFullId());
assertEquals("somemod", blockState.getNamespace());
assertEquals("someblock", blockState.getId());
assertTrue(blockState.getProperties().isEmpty());
blockState = BlockState.fromString("somemod:someblock[testProp=testVal,testProp2=testVal2]");
assertEquals("somemod:someblock", blockState.getFullId());
assertEquals("somemod", blockState.getNamespace());
assertEquals("someblock", blockState.getId());
assertEquals("testVal", blockState.getProperties().get("testProp"));
assertEquals("testVal2", blockState.getProperties().get("testProp2"));
}
}

View File

@ -179,6 +179,8 @@ public synchronized void load() throws ExecutionException, IOException, Interrup
resourcePack.loadBlockColorConfig(blockColorsConfigFile);
resourcePack.saveTextureFile(textureExportFile);
configManager.getBlockPropertiesConfig().setResourcePack(resourcePack);
//load maps
for (MapConfig mapConfig : config.getMapConfigs()) {
String id = mapConfig.getId();