Make loading of custom block definitions much more forgiving

master
Captain Chaos 2022-08-23 10:36:20 +02:00
parent ee4f8aa0d5
commit 2d841d7c3c
4 changed files with 221 additions and 61 deletions

View File

@ -8,7 +8,6 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.pepsoft.util.CSVDataSource;
import org.pepsoft.util.Pair;
import org.pepsoft.worldpainter.Configuration;
import org.pepsoft.worldpainter.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,6 +30,7 @@ import static org.pepsoft.minecraft.Constants.*;
import static org.pepsoft.minecraft.HorizontalOrientationScheme.CARDINAL_DIRECTIONS;
import static org.pepsoft.minecraft.HorizontalOrientationScheme.STAIR_CORNER;
import static org.pepsoft.minecraft.Material.PropertyType.*;
import static org.pepsoft.minecraft.MaterialImporter.importCustomMaterials;
import static org.pepsoft.util.ObjectMapperHolder.OBJECT_MAPPER;
import static org.pepsoft.worldpainter.Constants.UNKNOWN_MATERIAL_COLOUR;
import static org.pepsoft.worldpainter.Platform.Capability.NAME_BASED;
@ -1615,64 +1615,7 @@ public final class Material implements Serializable {
throw new RuntimeException("I/O error while reading Minecraft materials database materials.csv from classpath", e);
}
final File customMaterialsDir = new File(Configuration.getConfigDir(), "materials");
if (customMaterialsDir.isDirectory()) {
final File[] customSpecFiles = customMaterialsDir.listFiles(pathname -> pathname.isFile() && pathname.getName().toLowerCase().endsWith(".csv"));
if (customSpecFiles != null) {
for (File customSpecFile: customSpecFiles) {
int count = 0;
final Set<String> namespaces = new HashSet<>();
try (Reader in = new InputStreamReader(new FileInputStream(customSpecFile), UTF_8)) {
CSVDataSource csvDataSource = new CSVDataSource();
csvDataSource.openForReading(in);
do {
Map<String, Object> materialSpecs = new HashMap<>();
String name = csvDataSource.getString("name");
materialSpecs.put("name", name);
String str = csvDataSource.getString("discriminator");
if (! isNullOrEmpty(str)) {
materialSpecs.put("discriminator", ImmutableSet.copyOf(str.split(",")));
}
str = csvDataSource.getString("properties");
if (! isNullOrEmpty(str)) {
materialSpecs.put("properties", stream(str.split(",")).map(PropertyDescriptor::fromString).collect(toMap(d -> d.name, identity())));
}
materialSpecs.put("opacity", csvDataSource.getInt("opacity"));
materialSpecs.put("receivesLight", csvDataSource.getBoolean("receivesLight"));
materialSpecs.put("terrain", false);
materialSpecs.put("insubstantial", csvDataSource.getBoolean("insubstantial"));
materialSpecs.put("veryInsubstantial", csvDataSource.getBoolean("insubstantial")); // Copy "insubstantial"
materialSpecs.put("resource", csvDataSource.getBoolean("resource"));
materialSpecs.put("tileEntity", csvDataSource.getBoolean("tileEntity"));
str = csvDataSource.getString("tileEntityId");
if (! isNullOrEmpty(str)) {
materialSpecs.put("tileEntityId", str);
}
materialSpecs.put("treeRelated", csvDataSource.getBoolean("treeRelated"));
materialSpecs.put("vegetation", csvDataSource.getBoolean("vegetation"));
materialSpecs.put("blockLight", csvDataSource.getInt("blockLight"));
materialSpecs.put("natural", csvDataSource.getBoolean("natural"));
materialSpecs.put("watery", csvDataSource.getBoolean("watery"));
str = csvDataSource.getString("colour");
if (! isNullOrEmpty(str)) {
materialSpecs.put("colour", Integer.parseUnsignedInt(str, 16));
}
MATERIAL_SPECS.computeIfAbsent(name, s -> new HashSet<>()).add(materialSpecs);
final int p = name.indexOf(':');
final String namespace = name.substring(0, p);
namespaces.add(namespace);
SIMPLE_NAMES_BY_NAMESPACE.computeIfAbsent(namespace, s -> new HashSet<>()).add(name.substring(p + 1));
csvDataSource.next();
count++;
} while (! csvDataSource.isEndOfFile());
} catch (RuntimeException | IOException e) {
throw new RuntimeException("I/O error while reading Minecraft materials database materials.csv from classpath", e);
}
logger.info("Loaded {} custom block(s) with namespace(s) {} from {}", count, namespaces, customSpecFile.getName());
}
}
}
importCustomMaterials(MATERIAL_SPECS, SIMPLE_NAMES_BY_NAMESPACE);
}
private static final Set<String> SNOW_ON = ImmutableSet.of(MC_SNOW_BLOCK, MC_POWDER_SNOW);

View File

@ -0,0 +1,149 @@
package org.pepsoft.minecraft;
import com.google.common.collect.ImmutableSet;
import org.pepsoft.util.CSVDataSource;
import org.pepsoft.worldpainter.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static org.pepsoft.minecraft.Constants.MC_DIRT_PATH;
import static org.pepsoft.minecraft.Constants.MC_GRASS_PATH;
public class MaterialImporter {
static void importCustomMaterials(Map<String, Set<Map<String, Object>>> materialSpecs, Map<String, Set<String>> simpleNamesByNamespace) {
final File customMaterialsDir = new File(Configuration.getConfigDir(), "materials");
if (customMaterialsDir.isDirectory()) {
final File[] customSpecFiles = customMaterialsDir.listFiles(pathname -> pathname.isFile() && pathname.getName().toLowerCase().endsWith(".csv"));
if (customSpecFiles != null) {
for (File customSpecFile: customSpecFiles) {
int count = 0;
final Set<String> namespaces = new HashSet<>();
try (Reader in = new InputStreamReader(new FileInputStream(customSpecFile), UTF_8)) {
CSVDataSource csvDataSource = new CSVDataSource();
csvDataSource.openForReading(in);
do {
Map<String, Object> myMaterialSpecs = new HashMap<>();
String name = csvDataSource.getString("name");
myMaterialSpecs.put("name", name);
String str = csvDataSource.getString("discriminator", null);
if (! isNullOrEmpty(str)) {
myMaterialSpecs.put("discriminator", ImmutableSet.copyOf(str.split(",")));
}
str = csvDataSource.getString("properties", null);
if (! isNullOrEmpty(str)) {
myMaterialSpecs.put("properties", stream(str.split(",")).map(Material.PropertyDescriptor::fromString).collect(toMap(d -> d.name, identity())));
}
myMaterialSpecs.put("opacity", csvDataSource.getInt("opacity", guessOpacity(name)));
myMaterialSpecs.put("receivesLight", csvDataSource.getBoolean("receivesLight", guessReceivesLight(name)));
myMaterialSpecs.put("terrain", false);
final boolean insubstantial = csvDataSource.getBoolean("insubstantial", guessInsubstantial(name));
myMaterialSpecs.put("insubstantial", insubstantial);
myMaterialSpecs.put("veryInsubstantial", csvDataSource.getBoolean("insubstantial", insubstantial || guessVeryInsubstantial(name)));
myMaterialSpecs.put("resource", csvDataSource.getBoolean("resource", guessResource(name)));
final boolean tileEntity = csvDataSource.getBoolean("tileEntity", false);
myMaterialSpecs.put("tileEntity", tileEntity);
if (tileEntity) {
myMaterialSpecs.put("tileEntityId", csvDataSource.getString("tileEntityId"));
}
final boolean treeRelated = csvDataSource.getBoolean("treeRelated", guessTreeRelated(name));
myMaterialSpecs.put("treeRelated", treeRelated);
final boolean vegetation = csvDataSource.getBoolean("vegetation", (! treeRelated) && guessVegetation(name));
myMaterialSpecs.put("vegetation", vegetation);
myMaterialSpecs.put("blockLight", csvDataSource.getInt("blockLight", 0));
myMaterialSpecs.put("natural", csvDataSource.getBoolean("natural", vegetation || treeRelated || guessNatural(name)));
myMaterialSpecs.put("watery", csvDataSource.getBoolean("watery", false));
str = csvDataSource.getString("colour", null);
if (! isNullOrEmpty(str)) {
try {
myMaterialSpecs.put("colour", Integer.parseUnsignedInt(str, 16));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Not a valid hexadecimal integer value for column colour: \"" + str + "\"", e);
}
}
materialSpecs.computeIfAbsent(name, s -> new HashSet<>()).add(myMaterialSpecs);
final int p = name.indexOf(':');
final String namespace = name.substring(0, p);
namespaces.add(namespace);
simpleNamesByNamespace.computeIfAbsent(namespace, s -> new HashSet<>()).add(name.substring(p + 1));
csvDataSource.next();
count++;
} while (! csvDataSource.isEndOfFile());
logger.info("Loaded {} custom block(s) with namespace(s) {} from {}", count, namespaces, customSpecFile.getName());
} catch (RuntimeException | IOException e) {
final String message = String.format("%s while reading custom block definition(s) from %s\nMessage: %s", e.getClass().getSimpleName(), customSpecFile.getName(), e.getMessage());
logger.error(message, e);
errors.add(message);
}
}
}
}
}
public static int guessOpacity(String name) {
if (name.endsWith("_slab") || name.endsWith("_stairs") || name.contains("block") || name.endsWith("_log") || name.endsWith("_wood") || name.endsWith("_stem") || name.endsWith("_hyphea") || name.contains("bricks")) {
return 15;
} else if (name.contains("leaves")) {
return 1;
} else {
return 0;
}
}
public static boolean guessResource(String name) {
return name.contains("ore");
}
public static boolean guessTreeRelated(String name) {
return name.endsWith("_log") || name.endsWith("_wood") || name.endsWith("_stem") || name.endsWith("_hyphea") || name.endsWith("_leaves") || name.endsWith("_sapling");
}
public static boolean guessVeryInsubstantial(String name) {
return guessVegetation(name) || name.contains("leaves");
}
public static boolean guessInsubstantial(String name) {
return guessVegetation(name);
}
public static boolean guessVegetation(String name) {
return (! name.endsWith("_block"))
&& (! guessTreeRelated(name))
&& (name.contains("leaf")
|| name.contains("vine")
|| name.contains("fungus")
|| name.contains("roots")
|| name.contains("azalea")
|| name.contains("flowering")
|| name.contains("lichen")
|| name.contains("moss")
|| name.contains("stem")
|| name.contains("blossom"));
}
public static boolean guessNatural(String material) {
return (guessVegetation(material) || guessTreeRelated(material))
&& (! material.contains("stripped"));
}
/**
* Guess whether a material receives light unto itself, despite being opaque to surrounding blocks.
*/
public static boolean guessReceivesLight(String name) {
return NON_TRANSMITTING_TRANSPARENT_BLOCKS.contains(name)
|| name.endsWith("_slab")
|| name.endsWith("_stairs");
}
public static final List<String> errors = new ArrayList<>();
private static final Set<String> NON_TRANSMITTING_TRANSPARENT_BLOCKS = ImmutableSet.of(MC_DIRT_PATH, MC_GRASS_PATH);
private static final Logger logger = LoggerFactory.getLogger(MaterialImporter.class);
}

View File

@ -12,6 +12,7 @@ import ch.qos.logback.core.util.StatusPrinter;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.utils.Lm;
import org.intellij.lang.annotations.Language;
import org.pepsoft.minecraft.MaterialImporter;
import org.pepsoft.util.DesktopUtils;
import org.pepsoft.util.FileUtils;
import org.pepsoft.util.GUIUtils;
@ -50,6 +51,7 @@ import java.util.*;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static org.pepsoft.util.GUIUtils.getUIScale;
import static org.pepsoft.worldpainter.Constants.ATTRIBUTE_KEY_PLUGINS;
import static org.pepsoft.worldpainter.Constants.ATTRIBUTE_KEY_SAFE_MODE;
@ -511,6 +513,10 @@ public class Main {
if (myConfig.isAutosaveEnabled() && autosaveInhibited) {
JOptionPane.showMessageDialog(app, "Another instance of WorldPainter is already running.\nAutosave will therefore be disabled in this instance of WorldPainter!", "Autosave Disabled", JOptionPane.WARNING_MESSAGE);
}
for (String error: MaterialImporter.errors) {
DesktopUtils.beep();
JOptionPane.showMessageDialog(app, error, "Custom Object Definition Error", ERROR_MESSAGE);
}
if (! DonationDialog.maybeShowDonationDialog(app)) {
MerchDialog.maybeShowMerchDialog(app);
}
@ -532,7 +538,7 @@ public class Main {
// Report the error
logger.error("Exception while initialising configuration", e);
JOptionPane.showMessageDialog(null, "Could not read configuration file! Resetting configuration.\n\nException type: " + e.getClass().getSimpleName() + "\nMessage: " + e.getMessage(), "Configuration Error", JOptionPane.ERROR_MESSAGE);
JOptionPane.showMessageDialog(null, "Could not read configuration file! Resetting configuration.\n\nException type: " + e.getClass().getSimpleName() + "\nMessage: " + e.getMessage(), "Configuration Error", ERROR_MESSAGE);
}
@Language("HTML")

View File

@ -9,6 +9,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
@ -96,9 +97,21 @@ public class CSVDataSource {
* @return The value of the specified column in the current row.
*/
public String getString(String columnName) {
checkColumnName(columnName);
return getString(columnsByName.get(columnName));
}
/**
* Get a string-typed value by column name.
*
* @param columnName The name of the column.
* @param defaultValue The value to return if the specified column is not present, or the value is not set.
* @return The value of the specified column in the current row.
*/
public String getString(String columnName, String defaultValue) {
return columnsByName.containsKey(columnName) ? getString(columnsByName.get(columnName), defaultValue) : defaultValue;
}
/**
* Get a string-typed value by column index.
*
@ -109,6 +122,18 @@ public class CSVDataSource {
return currentRow.get(columnIndex);
}
/**
* Get a string-typed value by column index.
*
* @param columnIndex The index of the column.
* @return The value of the specified column in the current row, or {@code defaultValue} if the column does not
* exist or the value is not set.
*/
public String getString(int columnIndex, String defaultValue) {
final String value = currentRow.get(columnIndex);
return isNullOrEmpty(value) ? defaultValue : value;
}
/**
* Set a string-typed value by column name. {@code null} values are
* supported but are converted into empty strings.
@ -117,6 +142,7 @@ public class CSVDataSource {
* @param value The value to store in the column.
*/
public void setString(String columnName, String value) {
checkColumnName(columnName);
setString(columnsByName.get(columnName), value);
}
@ -132,21 +158,57 @@ public class CSVDataSource {
}
public int getInt(String columnName) {
return Integer.parseInt(getString(columnName));
checkColumnName(columnName);
try {
return Integer.parseInt(getString(columnName));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Not a valid integer value for column " + columnName + ": \"" + getString(columnName) + "\"", e);
}
}
public int getInt(String columnName, int defaultValue) {
if (! columnsByName.containsKey(columnName)) {
return defaultValue;
} else {
final String stringValue = getString(columnName);
try {
return (stringValue != null) ? Integer.parseInt(stringValue) : defaultValue;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Not a valid integer value for column " + columnName + ": \"" + getString(columnName) + "\"", e);
}
}
}
public void setInt(String columnName, int value) {
checkColumnName(columnName);
setString(columnName, Integer.toString(value));
}
public boolean getBoolean(String columnName) {
checkColumnName(columnName);
return Boolean.parseBoolean(getString(columnName));
}
public boolean getBoolean(String columnName, boolean defaultValue) {
if (! columnsByName.containsKey(columnName)) {
return defaultValue;
} else {
final String stringValue = getString(columnName);
return (stringValue != null) ? Boolean.parseBoolean(stringValue) : defaultValue;
}
}
public void setBoolean(String columnName, boolean value) {
checkColumnName(columnName);
setString(columnName, Boolean.toString(value));
}
private void checkColumnName(String columnName) {
if (! columnsByName.containsKey(columnName)) {
throw new IllegalArgumentException("There is no column named \"" + columnName + "\"");
}
}
private void readHeaders() throws IOException {
List<String> headers = readLine();
columnsByName = new HashMap<>();