package org.pepsoft.worldpainter; import org.pepsoft.minecraft.Material; import org.pepsoft.util.GUIUtils; import org.pepsoft.util.IconUtils; import org.pepsoft.util.PerlinNoise; import org.pepsoft.worldpainter.heightMaps.NoiseHeightMap; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; import java.io.Serializable; import java.util.*; import java.util.concurrent.Callable; import static org.pepsoft.util.GUIUtils.*; /** * @author SchmitzP */ public class MixedMaterial implements Serializable, Comparable<MixedMaterial> { /** * Create a new "mixed material" which contains only one material. * * @param name The name of the mixed material. * @param row A single row describing the material. * @param biome The default biome associated with this mixed material, or -1 * for no default biome. * @param colour The colour associated with this mixed material, or * <code>null</code> for no default colour. */ public MixedMaterial(final String name, final Row row, final int biome, final Integer colour) { this(name, new Row[] {row}, biome, Mode.SIMPLE, 1.0f, colour, null, 0, 0, false); } /** * Create a new noisy mixed material. * * @param name The name of the mixed material. * @param rows The rows describing the materials to be used together with * their occurrences. * @param biome The default biome associated with this mixed material, or -1 * for no default biome. * @param colour The colour associated with this mixed material, or * <code>null</code> for no default colour. */ public MixedMaterial(final String name, final Row[] rows, final int biome, final Integer colour) { this(name, rows, biome, Mode.NOISE, 1.0f, colour, null, 0, 0, false); } /** * Create a new blobby mixed material. * * @param name The name of the mixed material. * @param rows The rows describing the materials to be used together with * their occurrences. * @param biome The default biome associated with this mixed material, or -1 * for no default biome. * @param colour The colour associated with this mixed material, or * <code>null</code> for no default colour. * @param scale The scale of the blobs. <code>1.0f</code> for default size. */ public MixedMaterial(final String name, final Row[] rows, final int biome, final Integer colour, final float scale) { this(name, rows, biome, Mode.BLOBS, scale, colour, null, 0, 0, false); } /** * Create a new layered mixed material. * * @param name The name of the mixed material. * @param rows The rows describing the materials to be used together with * their heights. * @param biome The default biome associated with this mixed material, or -1 * for no default biome. * @param colour The colour associated with this mixed material, or * <code>null</code> for no default colour. * @param variation The variation in layer height which should be applied, * or <code>null</code> for no variation. * @param layerXSlope The slope of the layer for the x-axis. * Must be zero if <code>repeat</code> is false. * @param layerYSlope The slope of the layer for the y-axis. * Must be zero if <code>repeat</code> is false. * @param repeat Whether the layers should repeat vertically. */ public MixedMaterial(final String name, final Row[] rows, final int biome, final Integer colour, final NoiseSettings variation, final double layerXSlope, final double layerYSlope, final boolean repeat) { this(name, rows, biome, Mode.LAYERED, 1.0f, colour, variation, layerXSlope, layerYSlope, repeat); } MixedMaterial(final String name, final Row[] rows, final int biome, final Mode mode, final float scale, final Integer colour, final NoiseSettings variation, final double layerXSlope, final double layerYSlope, final boolean repeat) { if ((mode != Mode.LAYERED) && (mode != Mode.SIMPLE)) { int total = 0; for (Row row: rows) { total += row.occurrence; } if (total != 1000) { throw new IllegalArgumentException("Total occurrence is not 1000"); } } this.name = name; this.rows = rows; this.biome = biome; this.mode = mode; this.scale = scale; this.colour = colour; this.variation = variation; this.layerXSlope = layerXSlope; this.layerYSlope = layerYSlope; this.repeat = repeat; init(); } public UUID getId() { return id; } public String getName() { return name; } public int getBiome() { return biome; } public Mode getMode() { return mode; } public float getScale() { return scale; } public Integer getColour() { return colour; } public NoiseSettings getVariation() { return variation; } public boolean isRepeat() { return repeat; } public double getLayerXSlope() { return layerXSlope; } public double getLayerYSlope() { return layerYSlope; } public BufferedImage getIcon(ColourScheme colourScheme) { if (colourScheme != null) { final BufferedImage icon = new BufferedImage(16 * UI_SCALE, 16 * UI_SCALE, BufferedImage.TYPE_INT_RGB); // Draw the terrain if (colour != null) { for (int x = 1; x < 16 * UI_SCALE - 1; x++) { for (int y = 1; y < 16 * UI_SCALE - 1; y++) { icon.setRGB(x, y, colour); } } } else { for (int x = 1; x < 16 * UI_SCALE - 1; x++) { for (int y = 1; y < 16 * UI_SCALE - 1; y++) { icon.setRGB(x, y, colourScheme.getColour(getMaterial(0, x / 2, 0, 15 - y / 2f))); } } } return icon; } else { return UNKNOWN_ICON; } } public Material getMaterial(long seed, int x, int y, float z) { switch (mode) { case SIMPLE: return simpleMaterial; case NOISE: return materials[random.nextInt(1000)]; case BLOBS: double xx = x / Constants.TINY_BLOBS, yy = y / Constants.TINY_BLOBS, zz = z / Constants.TINY_BLOBS; if (seed + 1 != noiseGenerators[0].getSeed()) { for (int i = 0; i < noiseGenerators.length; i++) { noiseGenerators[i].setSeed(seed + i + 1); } } Material material = sortedRows[sortedRows.length - 1].material; for (int i = noiseGenerators.length - 1; i >= 0; i--) { final float rowScale = sortedRows[i].scale * this.scale; if (noiseGenerators[i].getPerlinNoise(xx / rowScale, yy / rowScale, zz / rowScale) >= sortedRows[i].chance) { material = sortedRows[i].material; } } return material; case LAYERED: if (layerNoiseheightMap != null) { if (layerNoiseheightMap.getSeed() != seed) { layerNoiseheightMap.setSeed(seed); } z += layerNoiseheightMap.getValue(x, y, z) - layerNoiseOffset; } if (repeat) { if (layerXSlope != 0.0) { z += layerXSlope * x; } if (layerYSlope != 0.0) { z += layerYSlope * y; } return materials[Math.floorMod((int) (z + 0.5f), materials.length)]; } else { final int iZ = (int) (z + 0.5f); if (iZ < 0) { return materials[0]; } else if (iZ >= materials.length) { return materials[materials.length - 1]; } else { return materials[iZ]; } } default: throw new InternalError(); } } public Material getMaterial(long seed, int x, int y, int z) { switch (mode) { case SIMPLE: return simpleMaterial; case NOISE: return materials[random.nextInt(1000)]; case BLOBS: double xx = x / Constants.TINY_BLOBS, yy = y / Constants.TINY_BLOBS, zz = z / Constants.TINY_BLOBS; if (seed + 1 != noiseGenerators[0].getSeed()) { for (int i = 0; i < noiseGenerators.length; i++) { noiseGenerators[i].setSeed(seed + i + 1); } } Material material = sortedRows[sortedRows.length - 1].material; for (int i = noiseGenerators.length - 1; i >= 0; i--) { final float rowScale = sortedRows[i].scale * this.scale; if (noiseGenerators[i].getPerlinNoise(xx / rowScale, yy / rowScale, zz / rowScale) >= sortedRows[i].chance) { material = sortedRows[i].material; } } return material; case LAYERED: float fZ = z; if (layerNoiseheightMap != null) { if (layerNoiseheightMap.getSeed() != seed) { layerNoiseheightMap.setSeed(seed); } fZ += layerNoiseheightMap.getValue(x, y, z) - layerNoiseOffset; } if (repeat) { if (layerXSlope != 0.0) { fZ += layerXSlope * x; } if (layerYSlope != 0.0) { fZ += layerYSlope * y; } return materials[Math.floorMod((int) (fZ + 0.5f), materials.length)]; } else { final int iZ = (int) (fZ + 0.5f); if (iZ < 0) { return materials[0]; } else if (iZ >= materials.length) { return materials[materials.length - 1]; } else { return materials[iZ]; } } default: throw new InternalError(); } } public Row[] getRows() { return Arrays.copyOf(rows, rows.length); } void edit(final String name, final Row[] rows, final int biome, final Mode mode, final float scale, final Integer colour, final NoiseSettings variation, final double layerXSlope, final double layerYSlope, final boolean repeat) { if ((mode != Mode.LAYERED) && (mode != Mode.SIMPLE)) { int total = 0; for (Row row: rows) { total += row.occurrence; } if (total != 1000) { throw new IllegalArgumentException("Total occurrence is not 1000"); } } this.name = name; this.rows = rows; this.biome = biome; this.mode = mode; this.scale = scale; this.colour = colour; this.variation = variation; this.layerXSlope = layerXSlope; this.layerYSlope = layerYSlope; this.repeat = repeat; init(); } // Comparable @Override public int compareTo(MixedMaterial o) { if (name != null) { return (o.name != null) ? name.compareTo(o.name) : 1; } else { return (o.name != null) ? -1 : 0; } } // java.lang.Object @Override public String toString() { return name; } @Override public int hashCode() { int hash = 7; hash = 19 * hash + this.biome; hash = 19 * hash + Arrays.deepHashCode(this.rows); hash = 19 * hash + mode.hashCode(); hash = 19 * hash + Float.floatToIntBits(this.scale); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final MixedMaterial other = (MixedMaterial) obj; if (this.biome != other.biome) { return false; } if (!Arrays.deepEquals(this.rows, other.rows)) { return false; } if (this.mode != other.mode) { return false; } if (Float.floatToIntBits(this.scale) != Float.floatToIntBits(other.scale)) { return false; } return true; } /** * Utility method for creating a simple mixed material, consisting of one * block type with data value 0. * * @param blockType The block type the mixed material should consist of * @return A new mixed material with the specified block type, and the * block type's name */ public static MixedMaterial create(final int blockType) { return create(Material.get(blockType)); } /** * Utility method for creating a simple mixed material, consisting of one * material. * * @param material The simple material the mixed material should consist of * @return A new mixed material with the specified material and an * appropriate name */ public static MixedMaterial create(final Material material) { return new MixedMaterial(material.toString(), new Row(material, 1000, 1.0f), -1, null); } /** * Perform a task during which any new materials deserialised <em>on the * same thread</em> will be duplicated and given new identities, instead of * being replaced with existing instances with the same identity if * available. * * @param task The task to perform. * @param <V> The return type of the task. May be {@link Void} for tasks * which do not return a value. * @return The return value of the task or <code>null</code> if it does not * return a value. * @throws RuntimeException If the task throws a checked exception it will * be wrapped in a <code>RuntimeException</code>. */ public static <V> V duplicateNewMaterialsWhile(Callable<V> task) { DUPLICATE_MATERIALS.set(true); try { return task.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } finally { DUPLICATE_MATERIALS.set(false); } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // Legacy if (mode == null) { if (rows.length == 1) { mode = Mode.SIMPLE; } else if (noise) { mode = Mode.NOISE; } else { mode = Mode.BLOBS; } } init(); } private Object readResolve() throws ObjectStreamException { return DUPLICATE_MATERIALS.get() ? MixedMaterialManager.getInstance().registerAsNew(this) : MixedMaterialManager.getInstance().register(this); } private void init() { switch (mode) { case SIMPLE: if (rows.length != 1) { throw new IllegalArgumentException("Only one row allowed for SIMPLE mode"); } simpleMaterial = rows[0].material; break; case NOISE: if (rows.length < 2) { throw new IllegalArgumentException("Multiple rows required for NOISE mode"); } materials = new Material[1000]; int index = 0; for (Row row: rows) { for (int i = 0; i < row.occurrence; i++) { materials[index++] = row.material; } } random = new Random(); break; case BLOBS: if (rows.length < 2) { throw new IllegalArgumentException("Multiple rows required for BLOBS mode"); } sortedRows = Arrays.copyOf(rows, rows.length); Arrays.sort(sortedRows, (r1, r2) -> r1.occurrence - r2.occurrence); noiseGenerators = new PerlinNoise[rows.length - 1]; int cumulativePermillage = 0; for (int i = 0; i < noiseGenerators.length; i++) { noiseGenerators[i] = new PerlinNoise(0); cumulativePermillage += sortedRows[i].occurrence * (1000 - cumulativePermillage) / 1000; sortedRows[i].chance = PerlinNoise.getLevelForPromillage(cumulativePermillage); } break; case LAYERED: if (rows.length < 2) { throw new IllegalArgumentException("Multiple rows required for LAYERED mode"); } if ((! repeat) && ((layerXSlope != 0) || (layerYSlope != 0))) { throw new IllegalArgumentException("Angle may not be non-zero if repeat is false"); } List<Material> tmpMaterials = new ArrayList<>(org.pepsoft.minecraft.Constants.DEFAULT_MAX_HEIGHT_2); for (int i = rows.length - 1; i >= 0; i--) { for (int j = 0; j < rows[i].occurrence; j++) { tmpMaterials.add(rows[i].material); } } materials = tmpMaterials.toArray(new Material[tmpMaterials.size()]); if (variation != null) { layerNoiseheightMap = new NoiseHeightMap(variation, NOISE_SEED_OFFSET); layerNoiseOffset = variation.getRange(); } else { layerNoiseheightMap = null; layerNoiseOffset = 0; } break; } } private final UUID id = UUID.randomUUID(); private String name; private int biome; private Row[] rows; @Deprecated private final boolean noise = false; private float scale; private Integer colour; private Mode mode = Mode.BLOBS; private NoiseSettings variation; private boolean repeat; private double layerXSlope, layerYSlope; private transient Row[] sortedRows; private transient PerlinNoise[] noiseGenerators; private transient Material[] materials; private transient Random random; private transient Material simpleMaterial; private transient NoiseHeightMap layerNoiseheightMap; private transient int layerNoiseOffset; public static class Row implements Serializable { public Row(Material material, int occurrence, float scale) { this.material = material; this.occurrence = occurrence; this.scale = scale; } @Override public int hashCode() { int hash = 5; hash = 23 * hash + (this.material != null ? this.material.hashCode() : 0); hash = 23 * hash + this.occurrence; hash = 23 * hash + Float.floatToIntBits(this.scale); hash = 23 * hash + Float.floatToIntBits(this.chance); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Row other = (Row) obj; if (this.material != other.material && (this.material == null || !this.material.equals(other.material))) { return false; } if (this.occurrence != other.occurrence) { return false; } if (Float.floatToIntBits(this.scale) != Float.floatToIntBits(other.scale)) { return false; } if (Float.floatToIntBits(this.chance) != Float.floatToIntBits(other.chance)) { return false; } return true; } public String toString() { return material.toString(); } final Material material; final int occurrence; final float scale; float chance; private static final long serialVersionUID = 1L; } public enum Mode {SIMPLE, BLOBS, NOISE, LAYERED} private static final BufferedImage UNKNOWN_ICON = IconUtils.loadScaledImage("org/pepsoft/worldpainter/icons/unknown_pattern.png"); private static final long NOISE_SEED_OFFSET = 55904327L; private static final ThreadLocal<Boolean> DUPLICATE_MATERIALS = ThreadLocal.withInitial(() -> false); private static final long serialVersionUID = 1L; }