Complete rewrite of resource loading, render-optimizations and fixes for resource-packs

This commit is contained in:
Blue (Lukas Rieger) 2019-12-29 16:23:43 +01:00
parent d607e49ede
commit ff00f3f1b6
50 changed files with 2436 additions and 1402 deletions

View File

@ -45,6 +45,7 @@ import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
@ -59,7 +60,7 @@ import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.render.TileRenderer;
import de.bluecolored.bluemap.core.render.hires.HiresModelManager;
import de.bluecolored.bluemap.core.render.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.web.BlueMapWebServer;
import de.bluecolored.bluemap.core.web.WebSettings;
@ -69,17 +70,19 @@ public class BlueMapCLI {
private ConfigurationFile configFile;
private Configuration config;
private File configFolder;
private ResourcePack resourcePack;
private boolean forceRender;
public BlueMapCLI(ConfigurationFile configFile, boolean forceRender) {
public BlueMapCLI(ConfigurationFile configFile, File configFolder, boolean forceRender) {
this.configFile = configFile;
this.config = configFile.getConfig();
this.configFolder = configFolder;
this.forceRender = forceRender;
this.resourcePack = null;
}
public void renderMaps() throws IOException, NoSuchResourceException {
public void renderMaps() throws IOException {
Preconditions.checkNotNull(resourcePack);
config.getWebDataPath().toFile().mkdirs();
@ -126,6 +129,10 @@ public class BlueMapCLI {
}
webSettings.save();
Logger.global.logInfo("Writing textures.json ...");
File textureExportFile = config.getWebDataPath().resolve("textures.json").toFile();
resourcePack.saveTextureFile(textureExportFile);
for (MapType map : maps.values()) {
Logger.global.logInfo("Rendering map '" + map.getId() + "' ...");
Logger.global.logInfo("Collecting tiles to render...");
@ -187,14 +194,25 @@ public class BlueMapCLI {
webserver.start();
}
private boolean loadResources() throws IOException, NoSuchResourceException {
private boolean loadResources() throws IOException, ParseResourceException {
Logger.global.logInfo("Loading resources...");
File defaultResourceFile = config.getDataPath().resolve("minecraft-client-" + ResourcePack.MINECRAFT_CLIENT_VERSION + ".jar").toFile();
File resourceExtensionsFile = config.getDataPath().resolve("resourceExtensions.zip").toFile();
File textureExportFile = config.getWebDataPath().resolve("textures.json").toFile();
if (!defaultResourceFile.exists()) {
if (!handleMissingResources(defaultResourceFile)) return false;
}
resourceExtensionsFile.delete();
FileUtils.copyURLToFile(BlueMapCLI.class.getResource("/resourceExtensions.zip"), resourceExtensionsFile, 10000, 10000);
File blockColorsConfigFile = new File(configFolder, "blockColors.json");
if (!blockColorsConfigFile.exists()) {
FileUtils.copyURLToFile(BlueMapCLI.class.getResource("/blockColors.json"), blockColorsConfigFile, 10000, 10000);
}
//find more resource packs
File resourcePackFolder = configFile.getFile().toPath().resolveSibling("resourcepacks").toFile();
resourcePackFolder.mkdirs();
@ -204,8 +222,13 @@ public class BlueMapCLI {
List<File> resources = new ArrayList<>(resourcePacks.length + 1);
resources.add(defaultResourceFile);
for (File file : resourcePacks) resources.add(file);
resources.add(resourceExtensionsFile);
resourcePack = new ResourcePack(resources, textureExportFile);
resourcePack = new ResourcePack();
if (textureExportFile.exists()) resourcePack.loadTextureFile(textureExportFile);
resourcePack.load(resources);
resourcePack.loadBlockColorConfig(blockColorsConfigFile);
resourcePack.saveTextureFile(textureExportFile);
return true;
}
@ -228,7 +251,7 @@ public class BlueMapCLI {
}
}
public static void main(String[] args) throws IOException, NoSuchResourceException {
public static void main(String[] args) throws IOException, ParseResourceException {
CommandLineParser parser = new DefaultParser();
try {
@ -241,12 +264,14 @@ public class BlueMapCLI {
}
//load config
File configFile = new File("bluemap.conf").getAbsoluteFile();
File configFolder = new File(".");
if (cmd.hasOption("c")) {
configFile = new File(cmd.getOptionValue("c"));
configFile.getParentFile().mkdirs();
configFolder = new File(cmd.getOptionValue("c"));
configFolder.mkdirs();
}
File configFile = new File(configFolder, "bluemap.conf");
boolean configCreated = !configFile.exists();
ConfigurationFile config = ConfigurationFile.loadOrCreate(configFile, BlueMapCLI.class.getResource("/bluemap-cli.conf"));
@ -256,7 +281,7 @@ public class BlueMapCLI {
return;
}
BlueMapCLI bluemap = new BlueMapCLI(config, cmd.hasOption("f"));
BlueMapCLI bluemap = new BlueMapCLI(config, configFolder, cmd.hasOption("f"));
if (config.getConfig().isWebserverEnabled()) {
//start webserver
@ -294,8 +319,8 @@ public class BlueMapCLI {
Option.builder("c")
.longOpt("config")
.hasArg()
.argName("config-file")
.desc("Sets path of the configuration file to use")
.argName("config-folder")
.desc("Sets path of the folder containing the configuration-files to use (configurations will be generated here if they don't exist)")
.build()
);

View File

@ -16,5 +16,13 @@ task zipWebroot(type: Zip) {
outputs.upToDateWhen { false }
}
task zipResourceExtensions(type: Zip) {
from fileTree('src/main/resourceExtensions')
archiveName 'resourceExtensions.zip'
destinationDir(file('/src/main/resources/'))
outputs.upToDateWhen { false }
}
//always update the zip before build
compileJava.dependsOn(zipWebroot)
compileJava.dependsOn(zipResourceExtensions)

View File

@ -24,46 +24,63 @@
*/
package de.bluecolored.bluemap.core.logger;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.Sets;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public abstract class AbstractLogger extends Logger {
private Set<String> noFloodLog;
private static final Object DUMMY = new Object();
private Cache<String, Object> noFloodCache;
public AbstractLogger() {
noFloodLog = Sets.newConcurrentHashSet();
noFloodCache = CacheBuilder.newBuilder()
.concurrencyLevel(4)
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10000)
.build();
}
@Override
public void noFloodError(String key, String message, Throwable throwable){
if (noFloodLog.add(key)) logError(message, throwable);
if (check(key)) logError(message, throwable);
}
@Override
public void noFloodWarning(String key, String message){
if (noFloodLog.add(key)) logWarning(message);
if (check(key)) logWarning(message);
}
@Override
public void noFloodInfo(String key, String message){
if (noFloodLog.add(key)) logInfo(message);
if (check(key)) logInfo(message);
}
@Override
public void noFloodDebug(String key, String message){
if (noFloodLog.add(key)) logDebug(message);
if (check(key)) logDebug(message);
}
@Override
public void clearNoFloodLog() {
noFloodLog.clear();
noFloodCache.invalidateAll();
noFloodCache.cleanUp();
}
@Override
public void removeNoFloodKey(String key) {
noFloodLog.remove(key);
noFloodCache.invalidate(key);
}
private boolean check(String key) {
if (noFloodCache.getIfPresent(key) == null) {
noFloodCache.put(key, DUMMY);
return true;
}
return false;
}
}

View File

@ -30,9 +30,26 @@ public class PrintStreamLogger extends AbstractLogger {
private PrintStream out, err;
boolean isDebug;
public PrintStreamLogger(PrintStream out, PrintStream err) {
this.out = out;
this.err = err;
this.isDebug = true;
}
public PrintStreamLogger(PrintStream out, PrintStream err, boolean debug) {
this.out = out;
this.err = err;
this.isDebug = debug;
}
public boolean isDebug() {
return isDebug;
}
public void setDebug(boolean debug) {
this.isDebug = debug;
}
@Override
@ -53,7 +70,17 @@ public class PrintStreamLogger extends AbstractLogger {
@Override
public void logDebug(String message) {
out.println("[DEBUG] " + message);
if (isDebug) out.println("[DEBUG] " + message);
}
@Override
public void noFloodDebug(String key, String message) {
if (isDebug) super.noFloodDebug(key, message);
}
@Override
public void noFloodDebug(String message) {
if (isDebug) super.noFloodDebug(message);
}
}

View File

@ -29,8 +29,9 @@ import java.io.IOException;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.mca.mapping.LightData;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.CompoundTag;
public abstract class Chunk {
@ -63,7 +64,7 @@ public abstract class Chunk {
public abstract LightData getLightData(Vector3i pos);
public abstract String getBiomeId(Vector3i pos);
public abstract Biome getBiome(Vector3i pos);
public static Chunk create(MCAWorld world, CompoundTag chunkTag) throws IOException {
int version = chunkTag.getInt("DataVersion");

View File

@ -28,8 +28,9 @@ import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.mca.mapping.BiomeIdMapper;
import de.bluecolored.bluemap.core.mca.mapping.BlockIdMapper;
import de.bluecolored.bluemap.core.mca.mapping.LightData;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.ListTag;
import net.querz.nbt.mca.MCAUtil;
@ -90,7 +91,7 @@ class ChunkAnvil112 extends Chunk {
}
@Override
public String getBiomeId(Vector3i pos) {
public Biome getBiome(Vector3i pos) {
int x = pos.getX() & 0xF; // Math.floorMod(pos.getX(), 16)
int z = pos.getZ() & 0xF;
int biomeByteIndex = z * 16 + x;

View File

@ -32,8 +32,9 @@ import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.mapping.BiomeIdMapper;
import de.bluecolored.bluemap.core.mca.mapping.LightData;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.ByteArrayTag;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.IntArrayTag;
@ -111,7 +112,7 @@ class ChunkAnvil113 extends Chunk {
}
@Override
public String getBiomeId(Vector3i pos) {
public Biome getBiome(Vector3i pos) {
int x = pos.getX() & 0xF; // Math.floorMod(pos.getX(), 16)
int z = pos.getZ() & 0xF;
int biomeByteIndex = z * 16 + x;
@ -148,7 +149,7 @@ class ChunkAnvil113 extends Chunk {
if (stateTag.containsKey("Properties")) {
CompoundTag propertiesTag = stateTag.getCompoundTag("Properties");
for (Entry<String, Tag<?>> property : propertiesTag) {
properties.put(property.getKey(), ((StringTag) property.getValue()).getValue());
properties.put(property.getKey().toLowerCase(), ((StringTag) property.getValue()).getValue().toLowerCase());
}
}

View File

@ -27,20 +27,21 @@ package de.bluecolored.bluemap.core.mca;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.mca.mapping.BlockProperties;
import de.bluecolored.bluemap.core.mca.mapping.LightData;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
public class MCABlock extends Block {
private MCAWorld world;
private BlockState blockState;
private LightData lightData;
private String biome;
private Biome biome;
private BlockProperties properties;
private Vector3i pos;
public MCABlock(MCAWorld world, BlockState blockState, LightData lightData, String biome, BlockProperties properties, Vector3i pos) {
public MCABlock(MCAWorld world, BlockState blockState, LightData lightData, Biome biome, BlockProperties properties, Vector3i pos) {
this.world = world;
this.blockState = blockState;
this.lightData = lightData;
@ -85,7 +86,7 @@ public class MCABlock extends Block {
}
@Override
public String getBiome() {
public Biome getBiome() {
return biome;
}

View File

@ -64,11 +64,12 @@ import de.bluecolored.bluemap.core.mca.mapping.BiomeIdMapper;
import de.bluecolored.bluemap.core.mca.mapping.BlockIdMapper;
import de.bluecolored.bluemap.core.mca.mapping.BlockProperties;
import de.bluecolored.bluemap.core.mca.mapping.BlockPropertyMapper;
import de.bluecolored.bluemap.core.mca.mapping.LightData;
import de.bluecolored.bluemap.core.util.AABB;
import de.bluecolored.bluemap.core.world.Biome;
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.LightData;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.WorldChunk;
import net.querz.nbt.CompoundTag;
@ -162,7 +163,7 @@ public class MCAWorld implements World {
Chunk chunk = getChunk(chunkPos);
BlockState blockState = getExtendedBlockState(chunk, pos);
LightData lightData = chunk.getLightData(pos);
String biome = chunk.getBiomeId(pos);
Biome biome = chunk.getBiome(pos);
BlockProperties properties = blockPropertyMapper.map(blockState);
return new MCABlock(this, blockState, lightData, biome, properties, pos);

View File

@ -25,21 +25,19 @@
package de.bluecolored.bluemap.core.mca.mapping;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;
import de.bluecolored.bluemap.core.world.Biome;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class BiomeIdMapper {
private static final String DEFAULT_BIOME = "ocean";
private String[] biomes;
private HashMap<Integer, Biome> biomes;
public BiomeIdMapper() throws IOException {
biomes = new String[256];
for (int i = 0; i < biomes.length; i++) {
biomes[i] = DEFAULT_BIOME;
}
biomes = new HashMap<>();
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.setURL(getClass().getResource("/biomes.json"))
@ -48,18 +46,17 @@ public class BiomeIdMapper {
ConfigurationNode node = loader.load();
for (Entry<Object, ? extends ConfigurationNode> e : node.getChildrenMap().entrySet()){
String biome = e.getKey().toString();
int id = e.getValue().getNode("id").getInt(-1);
if (id >= 0 && id < biomes.length) {
biomes[id] = biome;
}
String id = e.getKey().toString();
Biome biome = Biome.create(id, e.getValue());
biomes.put(biome.getOrdinal(), biome);
}
}
public String get(int id) {
if (id < 0 || id >= biomes.length) return DEFAULT_BIOME;
return biomes[id];
public Biome get(int id) {
Biome biome = biomes.get(id);
if (biome == null) return Biome.DEFAULT;
return biome;
}
public static BiomeIdMapper create() throws IOException {

View File

@ -33,6 +33,7 @@ import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.util.AABB;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.World;
@ -95,8 +96,8 @@ public class EmptyBlockContext implements ExtendedBlockContext {
}
@Override
public String getBiome() {
return "ocean";
public Biome getBiome() {
return Biome.DEFAULT;
}
}

View File

@ -34,9 +34,7 @@ import de.bluecolored.bluemap.core.render.WorldTile;
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;
import de.bluecolored.bluemap.core.resourcepack.InvalidResourceDeclarationException;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.NoSuchTextureException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.AABB;
import de.bluecolored.bluemap.core.util.MathUtils;
@ -84,9 +82,9 @@ public class HiresModelRenderer {
BlockStateModel blockModel;
try {
blockModel = modelFactory.createFrom(block.getBlock(), new SlicedWorldChunkBlockContext(chunk, new Vector3i(x, y, z), renderSettings.getSliceY()), renderSettings);
} catch (NoSuchResourceException | InvalidResourceDeclarationException | NoSuchTextureException e) {
} catch (NoSuchResourceException e) {
blockModel = new BlockStateModel();
Logger.global.noFloodDebug(block.getBlock().getId() + "-hiresModelRenderer-blockmodelerr", "Failed to create BlockModel for BlockState: " + block.getBlock() + " (" + e.toString() + ")");
Logger.global.noFloodDebug(block.getBlock().getFullId() + "-hiresModelRenderer-blockmodelerr", "Failed to create BlockModel for BlockState: " + block.getBlock() + " (" + e.toString() + ")");
}
blockModel.translate(new Vector3f(x, y, z).sub(min.toFloat()));

View File

@ -27,11 +27,11 @@ package de.bluecolored.bluemap.core.render.hires.blockmodel;
import de.bluecolored.bluemap.core.render.RenderSettings;
import de.bluecolored.bluemap.core.render.context.EmptyBlockContext;
import de.bluecolored.bluemap.core.render.context.ExtendedBlockContext;
import de.bluecolored.bluemap.core.resourcepack.BlockColorCalculator;
import de.bluecolored.bluemap.core.resourcepack.BlockStateResource;
import de.bluecolored.bluemap.core.resourcepack.InvalidResourceDeclarationException;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.NoSuchTextureException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resourcepack.TransformedBlockModelResource;
import de.bluecolored.bluemap.core.world.BlockState;
public class BlockStateModelFactory {
@ -42,7 +42,7 @@ public class BlockStateModelFactory {
this.resourcePack = resources;
}
public BlockStateModel createFrom(BlockState blockState) throws NoSuchResourceException, InvalidResourceDeclarationException, NoSuchTextureException {
public BlockStateModel createFrom(BlockState blockState) throws NoSuchResourceException {
return createFrom(blockState, EmptyBlockContext.instance(), new RenderSettings() {
@Override
public float getLightShadeMultiplier() {
@ -61,37 +61,49 @@ public class BlockStateModelFactory {
});
}
public BlockStateModel createFrom(BlockState blockState, ExtendedBlockContext context, RenderSettings renderSettings) throws NoSuchResourceException, InvalidResourceDeclarationException, NoSuchTextureException {
//air won't be rendered
public BlockStateModel createFrom(BlockState blockState, ExtendedBlockContext context, RenderSettings renderSettings) throws NoSuchResourceException {
//shortcut for air
if (
blockState.getId().equals("air") ||
blockState.getId().equals("cave_air") ||
blockState.getId().equals("void_air")
blockState.getFullId().equals("minecraft:air") ||
blockState.getFullId().equals("minecraft:cave_air") ||
blockState.getFullId().equals("minecraft:void_air")
) {
return new BlockStateModel();
}
// if it is a liquid, use the LiquidModelBuilder
if (
blockState.getId().equals("water") ||
blockState.getId().equals("lava")
){
return new LiquidModelBuilder(blockState, context, resourcePack, renderSettings).build();
}
// if no other model builder matched try to find a model definition from the resource-packs and use the default ResourceModelBuilder
BlockStateResource resource = resourcePack.getBlockStateResource(blockState);
BlockStateModel model = new ResourceModelBuilder(resource, context, resourcePack, renderSettings).build();
BlockStateModel model = createModel(blockState, context, renderSettings);
// if block is waterlogged
if (LiquidModelBuilder.isWaterlogged(blockState)) {
BlockStateModel watermodel = new LiquidModelBuilder(WATERLOGGED_BLOCKSTATE, context, resourcePack, renderSettings).build();
model.merge(watermodel);
model.merge(createModel(WATERLOGGED_BLOCKSTATE, context, renderSettings));
}
return model;
}
private BlockStateModel createModel(BlockState blockState, ExtendedBlockContext context, RenderSettings renderSettings) throws NoSuchResourceException {
BlockStateResource resource = resourcePack.getBlockStateResource(blockState);
BlockStateModel model = new BlockStateModel();
BlockColorCalculator colorCalculator = resourcePack.getBlockColorCalculator();
ResourceModelBuilder modelBuilder = new ResourceModelBuilder(renderSettings, context, colorCalculator);
LiquidModelBuilder liquidBuilder = new LiquidModelBuilder(renderSettings, context, blockState, colorCalculator);
for (TransformedBlockModelResource bmr : resource.getModels(blockState, context.getPosition())){
switch (bmr.getModel().getType()){
case LIQUID:
model.merge(liquidBuilder.build(blockState, bmr.getModel()));
break;
default:
model.merge(modelBuilder.build(bmr));
break;
}
}
return model;
}
private BlockState WATERLOGGED_BLOCKSTATE = new BlockState("minecraft:water");

View File

@ -36,8 +36,9 @@ import de.bluecolored.bluemap.core.model.Face;
import de.bluecolored.bluemap.core.model.Model;
import de.bluecolored.bluemap.core.render.RenderSettings;
import de.bluecolored.bluemap.core.render.context.ExtendedBlockContext;
import de.bluecolored.bluemap.core.resourcepack.NoSuchTextureException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resourcepack.BlockColorCalculator;
import de.bluecolored.bluemap.core.resourcepack.BlockModelResource;
import de.bluecolored.bluemap.core.resourcepack.Texture;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.world.BlockState;
@ -50,43 +51,73 @@ public class LiquidModelBuilder {
private static final HashSet<String> DEFAULT_WATERLOGGED_BLOCK_IDS = Sets.newHashSet(
"minecraft:seagrass",
"minecraft:tall_seagrass",
"minecraft:kelp"
"minecraft:kelp",
"minecraft:bubble_column"
);
private BlockState blockState;
private BlockState liquidBlockState;
private ExtendedBlockContext context;
private ResourcePack resourcePack;
private RenderSettings renderSettings;
private BlockColorCalculator colorCalculator;
private float[] heights;
public LiquidModelBuilder(BlockState blockState, ExtendedBlockContext context, ResourcePack resourcePack, RenderSettings renderSettings) {
this.blockState = blockState;
public LiquidModelBuilder(RenderSettings renderSettings, ExtendedBlockContext context, BlockState liquidBlockState, BlockColorCalculator colorCalculator) {
this.context = context;
this.resourcePack = resourcePack;
this.renderSettings = renderSettings;
this.heights = new float[]{14f, 14f, 14f, 14f};
this.liquidBlockState = liquidBlockState;
this.colorCalculator = colorCalculator;
}
public BlockStateModel build() throws NoSuchTextureException {
public BlockStateModel build(BlockState blockState, BlockModelResource bmr) {
if (this.renderSettings.isExcludeFacesWithoutSunlight() && context.getRelativeBlock(0, 0, 0).getSunLightLevel() == 0) return new BlockStateModel();
int level = getLiquidLevel(blockState);
float[] heights = new float[]{16f, 16f, 16f, 16f};
if (level >= 8 ||level == 0 && isLiquid(context.getRelativeBlock(0, 1, 0))){
this.heights = new float[]{16f, 16f, 16f, 16f};
return buildModel();
if (level < 8 && !(level == 0 && isLiquid(context.getRelativeBlock(0, 1, 0)))){
heights = new float[]{
getLiquidCornerHeight(-1, 0, -1),
getLiquidCornerHeight(-1, 0, 0),
getLiquidCornerHeight(0, 0, -1),
getLiquidCornerHeight(0, 0, 0)
};
}
this.heights = new float[]{
getLiquidCornerHeight(-1, 0, -1),
getLiquidCornerHeight(-1, 0, 0),
getLiquidCornerHeight(0, 0, -1),
getLiquidCornerHeight(0, 0, 0)
};
BlockStateModel model = new BlockStateModel();
Texture texture = bmr.getTexture("still");
return buildModel();
Vector3f[] c = new Vector3f[]{
new Vector3f( 0, 0, 0 ),
new Vector3f( 0, 0, 16 ),
new Vector3f( 16, 0, 0 ),
new Vector3f( 16, 0, 16 ),
new Vector3f( 0, heights[0], 0 ),
new Vector3f( 0, heights[1], 16 ),
new Vector3f( 16, heights[2], 0 ),
new Vector3f( 16, heights[3], 16 ),
};
int textureId = texture.getId();
Vector3f tintcolor = Vector3f.ONE;
if (liquidBlockState.getFullId().equals("minecraft:water")) {
tintcolor = colorCalculator.getWaterAverageColor(context);
}
createElementFace(model, Direction.DOWN, c[0], c[2], c[3], c[1], tintcolor, textureId);
createElementFace(model, Direction.UP, c[5], c[7], c[6], c[4], tintcolor, textureId);
createElementFace(model, Direction.NORTH, c[2], c[0], c[4], c[6], tintcolor, textureId);
createElementFace(model, Direction.SOUTH, c[1], c[3], c[7], c[5], tintcolor, textureId);
createElementFace(model, Direction.WEST, c[0], c[1], c[5], c[4], tintcolor, textureId);
createElementFace(model, Direction.EAST, c[3], c[2], c[6], c[7], tintcolor, textureId);
//scale down
model.transform(Matrix3f.createScaling(1f / 16f));
//calculate mapcolor
Vector4f mapcolor = texture.getColor();
mapcolor = mapcolor.mul(tintcolor.toVector4(0.5));
model.setMapColor(mapcolor);
return model;
}
private float getLiquidCornerHeight(int x, int y, int z){
@ -133,9 +164,9 @@ public class LiquidModelBuilder {
return isLiquid(block.getBlock());
}
private boolean isLiquid(BlockState blockstate){
if (blockstate.getId().equals(blockState.getId())) return true;
return LiquidModelBuilder.isWaterlogged(blockstate);
private boolean isLiquid(BlockState blockState){
if (blockState.getFullId().equals(liquidBlockState.getFullId())) return true;
return LiquidModelBuilder.isWaterlogged(blockState);
}
private float getLiquidBaseHeight(BlockState block){
@ -151,45 +182,7 @@ public class LiquidModelBuilder {
return 0;
}
private BlockStateModel buildModel() throws NoSuchTextureException {
BlockStateModel model = new BlockStateModel();
Vector3f[] c = new Vector3f[]{
new Vector3f( 0, 0, 0 ),
new Vector3f( 0, 0, 16 ),
new Vector3f( 16, 0, 0 ),
new Vector3f( 16, 0, 16 ),
new Vector3f( 0, heights[0], 0 ),
new Vector3f( 0, heights[1], 16 ),
new Vector3f( 16, heights[2], 0 ),
new Vector3f( 16, heights[3], 16 ),
};
int textureId = resourcePack.getTextureProvider().getTextureIndex("block/" + blockState.getId() + "_still");
Vector3f tintcolor = Vector3f.ONE;
if (blockState.getId().equals("water")) {
tintcolor = resourcePack.getBlockColorProvider().getBiomeWaterAverageColor(context);
}
createElementFace(model, Direction.DOWN, c[0], c[2], c[3], c[1], tintcolor, textureId);
createElementFace(model, Direction.UP, c[5], c[7], c[6], c[4], tintcolor, textureId);
createElementFace(model, Direction.NORTH, c[2], c[0], c[4], c[6], tintcolor, textureId);
createElementFace(model, Direction.SOUTH, c[1], c[3], c[7], c[5], tintcolor, textureId);
createElementFace(model, Direction.WEST, c[0], c[1], c[5], c[4], tintcolor, textureId);
createElementFace(model, Direction.EAST, c[3], c[2], c[6], c[7], tintcolor, textureId);
//scale down
model.transform(Matrix3f.createScaling(1f / 16f));
//calculate mapcolor
Vector4f mapcolor = resourcePack.getTextureProvider().getTexture("block/" + blockState.getId() + "_still").getColor();
mapcolor = mapcolor.mul(tintcolor.toVector4(1));
model.setMapColor(mapcolor);
return model;
}
private void createElementFace(Model model, Direction faceDir, Vector3f c0, Vector3f c1, Vector3f c2, Vector3f c3, Vector3f color, int textureId) throws NoSuchTextureException {
private void createElementFace(Model model, Direction faceDir, Vector3f c0, Vector3f c1, Vector3f c2, Vector3f c3, Vector3f color, int textureId) {
//face culling
Block bl = context.getRelativeBlock(faceDir);

View File

@ -29,6 +29,7 @@ import com.flowpowered.math.imaginary.Complexf;
import com.flowpowered.math.imaginary.Quaternionf;
import com.flowpowered.math.matrix.Matrix3f;
import com.flowpowered.math.vector.Vector2f;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector3i;
import com.flowpowered.math.vector.Vector4f;
@ -37,16 +38,12 @@ import de.bluecolored.bluemap.core.model.Face;
import de.bluecolored.bluemap.core.render.RenderSettings;
import de.bluecolored.bluemap.core.render.context.BlockContext;
import de.bluecolored.bluemap.core.render.context.ExtendedBlockContext;
import de.bluecolored.bluemap.core.resourcepack.BlockModelElementFaceResource;
import de.bluecolored.bluemap.core.resourcepack.BlockModelElementResource;
import de.bluecolored.bluemap.core.resourcepack.BlockColorCalculator;
import de.bluecolored.bluemap.core.resourcepack.BlockModelResource;
import de.bluecolored.bluemap.core.resourcepack.BlockStateResource;
import de.bluecolored.bluemap.core.resourcepack.NoSuchTextureException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resourcepack.TextureProvider.Texture;
import de.bluecolored.bluemap.core.resourcepack.BlockModelResource.Element.Rotation;
import de.bluecolored.bluemap.core.resourcepack.Texture;
import de.bluecolored.bluemap.core.resourcepack.TransformedBlockModelResource;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.util.WeighedArrayList;
import de.bluecolored.bluemap.core.world.Block;
/**
@ -58,49 +55,37 @@ public class ResourceModelBuilder {
private static final Vector3f NEG_HALF_3F = HALF_3F.negate();
private static final Vector2f HALF_2F = Vector2f.ONE.mul(0.5);
private BlockStateResource resource;
private ExtendedBlockContext context;
private ResourcePack resourcePack;
private RenderSettings renderSettings;
private BlockColorCalculator colorCalculator;
public ResourceModelBuilder(BlockStateResource resource, ExtendedBlockContext context, ResourcePack resourcePack, RenderSettings renderSettings) {
this.resource = resource;
public ResourceModelBuilder(RenderSettings renderSettings, ExtendedBlockContext context, BlockColorCalculator colorCalculator) {
this.context = context;
this.resourcePack = resourcePack;
this.renderSettings = renderSettings;
this.colorCalculator = colorCalculator;
}
public BlockStateModel build() throws NoSuchTextureException {
public BlockStateModel build(TransformedBlockModelResource bmr) {
BlockStateModel model = new BlockStateModel();
for (WeighedArrayList<BlockModelResource> bmrList : resource.getModelResources()){
BlockModelResource bmr = bmrList.get((int) Math.floor(MathUtils.hashToFloat(context.getPosition(), 23489756) * bmrList.size()));
model.merge(fromModelResource(bmr));
for (BlockModelResource.Element element : bmr.getModel().getElements()){
model.merge(fromModelElementResource(element, bmr));
}
if (!bmr.getRotation().equals(Vector2i.ZERO)) {
model.translate(NEG_HALF_3F);
model.rotate(Quaternionf.fromAxesAnglesDeg(
-bmr.getRotation().getX(),
-bmr.getRotation().getY(),
0
));
model.translate(HALF_3F);
}
return model;
}
private BlockStateModel fromModelResource(BlockModelResource bmr) throws NoSuchTextureException {
BlockStateModel model = new BlockStateModel();
for (BlockModelElementResource bmer : bmr.getElements()){
model.merge(fromModelElementResource(bmer));
}
model.translate(NEG_HALF_3F);
model.rotate(Quaternionf.fromAxesAnglesDeg(
-bmr.getXRot(),
-bmr.getYRot(),
0
));
model.translate(HALF_3F);
return model;
}
private BlockStateModel fromModelElementResource(BlockModelElementResource bmer) throws NoSuchTextureException {
private BlockStateModel fromModelElementResource(BlockModelResource.Element bmer, TransformedBlockModelResource bmr) {
BlockStateModel model = new BlockStateModel();
//create faces
@ -118,28 +103,31 @@ public class ResourceModelBuilder {
new Vector3f( max .getX(), max .getY(), max .getZ()),
};
createElementFace(model, bmer.getDownFace(), Direction.DOWN, c[0], c[2], c[3], c[1]);
createElementFace(model, bmer.getUpFace(), Direction.UP, c[5], c[7], c[6], c[4]);
createElementFace(model, bmer.getNorthFace(), Direction.NORTH, c[2], c[0], c[4], c[6]);
createElementFace(model, bmer.getSouthFace(), Direction.SOUTH, c[1], c[3], c[7], c[5]);
createElementFace(model, bmer.getWestFace(), Direction.WEST, c[0], c[1], c[5], c[4]);
createElementFace(model, bmer.getEastFace(), Direction.EAST, c[3], c[2], c[6], c[7]);
createElementFace(model, bmr, bmer, Direction.DOWN, c[0], c[2], c[3], c[1]);
createElementFace(model, bmr, bmer, Direction.UP, c[5], c[7], c[6], c[4]);
createElementFace(model, bmr, bmer, Direction.NORTH, c[2], c[0], c[4], c[6]);
createElementFace(model, bmr, bmer, Direction.SOUTH, c[1], c[3], c[7], c[5]);
createElementFace(model, bmr, bmer, Direction.WEST, c[0], c[1], c[5], c[4]);
createElementFace(model, bmr, bmer, Direction.EAST, c[3], c[2], c[6], c[7]);
//rotate
if (bmer.isRotation()){
Vector3f translation = bmer.getRotationOrigin();
Rotation rotation = bmer.getRotation();
if (rotation.getAngle() != 0f){
Vector3f translation = rotation.getOrigin();
model.translate(translation.negate());
Vector3f rotAxis = rotation.getAxis().toVector().toFloat();
model.rotate(Quaternionf.fromAngleDegAxis(
bmer.getRotationAngle(),
bmer.getRotationAxis().toVector().toFloat()
rotation.getAngle(),
rotAxis
));
if (bmer.isRotationRescale()){
if (rotation.isRescale()){
Vector3f scale =
Vector3f.ONE
.sub(bmer.getRotationAxis().toVector().toFloat())
.mul(Math.abs(TrigMath.sin(bmer.getRotationAngle() * TrigMath.DEG_TO_RAD)))
.sub(rotAxis)
.mul(Math.abs(TrigMath.sin(rotation.getAngle() * TrigMath.DEG_TO_RAD)))
.mul(1 - (TrigMath.SQRT_OF_TWO - 1))
.add(Vector3f.ONE);
model.transform(Matrix3f.createScaling(scale));
@ -155,19 +143,20 @@ public class ResourceModelBuilder {
return model;
}
private void createElementFace(BlockStateModel model, BlockModelElementFaceResource face, Direction faceDir, Vector3f c0, Vector3f c1, Vector3f c2, Vector3f c3) throws NoSuchTextureException {
private void createElementFace(BlockStateModel model, TransformedBlockModelResource modelResource, BlockModelResource.Element element, Direction faceDir, Vector3f c0, Vector3f c1, Vector3f c2, Vector3f c3) {
BlockModelResource.Element.Face face = element.getFaces().get(faceDir);
if (face == null) return;
BlockModelResource m = face.getElement().getModel();
//face culling
if (face.isCullface()){
Block b = getRotationRelativeBlock(m, face.getCullface());
if (face.getCullface() != null){
Block b = getRotationRelativeBlock(modelResource, face.getCullface());
if (b.isCullingNeighborFaces()) return;
}
//light calculation
Block b = getRotationRelativeBlock(m, faceDir);
BlockContext bContext = context.getRelativeView(getRotationRelativeDirectionVector(m, faceDir.toVector().toFloat()).toInt());
Block b = getRotationRelativeBlock(modelResource, faceDir);
BlockContext bContext = context.getRelativeView(getRotationRelativeDirectionVector(modelResource, faceDir.toVector().toFloat()).toInt());
float skyLight = b.getPassedSunLight(bContext);
//filter out faces that are not skylighted
@ -186,12 +175,13 @@ public class ResourceModelBuilder {
//UV-Lock counter-rotation
int uvLockAngle = 0;
if (m.isUvLock()){
Quaternionf rot = Quaternionf.fromAxesAnglesDeg(m.getXRot(), m.getYRot(), 0);
Vector2i rotation = modelResource.getRotation();
if (modelResource.isUVLock()){
Quaternionf rot = Quaternionf.fromAxesAnglesDeg(rotation.getX(), rotation.getY(), 0);
uvLockAngle = (int) rot.getAxesAnglesDeg().dot(faceDir.toVector().toFloat());
//TODO: my math has stopped working, there has to be a more consistent solution
if (m.getXRot() >= 180 && m.getYRot() != 90 && m.getYRot() != 270) uvLockAngle += 180;
if (rotation.getX() >= 180 && rotation.getY() != 90 && rotation.getY() != 270) uvLockAngle += 180;
}
//create both triangles
@ -205,27 +195,25 @@ public class ResourceModelBuilder {
uvs = rotateUVOuter(uvs, uvLockAngle);
uvs = rotateUVInner(uvs, face.getRotation());
String textureName = face.getResolvedTexture();
if (textureName == null) throw new NoSuchTextureException("There is no Texture-Definition for a face: " + faceDir + " of block: " + resource.getBlock());
int textureId = resourcePack.getTextureProvider().getTextureIndex(textureName);
Texture texture = face.getTexture();
int textureId = texture.getId();
Face f1 = new Face(c0, c1, c2, uvs[0], uvs[1], uvs[2], textureId);
Face f2 = new Face(c0, c2, c3, uvs[0], uvs[2], uvs[3], textureId);
//calculate ao
double ao0 = 1d, ao1 = 1d, ao2 = 1d, ao3 = 1d;
if (renderSettings.getAmbientOcclusionStrenght() > 0f && m.isAmbientOcclusion()){
ao0 = testAo(m, c0, faceDir);
ao1 = testAo(m, c1, faceDir);
ao2 = testAo(m, c2, faceDir);
ao3 = testAo(m, c3, faceDir);
if (renderSettings.getAmbientOcclusionStrenght() > 0f && modelResource.getModel().isAmbientOcclusion()){
ao0 = testAo(modelResource, c0, faceDir);
ao1 = testAo(modelResource, c1, faceDir);
ao2 = testAo(modelResource, c2, faceDir);
ao3 = testAo(modelResource, c3, faceDir);
}
//tint the face
Vector3f color = Vector3f.ONE;
if (face.isTinted()){
color = resourcePack.getBlockColorProvider().getBlockColor(context);
color = colorCalculator.getBlockColor(context); //TODO: cache this so we don't recalculate the tint color again for each face?
}
color = color.mul(light);
@ -251,50 +239,46 @@ public class ResourceModelBuilder {
model.addFace(f2);
//if is top face set model-color
Vector3f dir = getRotationRelativeDirectionVector(m, faceDir.toVector().toFloat());
Vector3f dir = getRotationRelativeDirectionVector(modelResource, faceDir.toVector().toFloat());
BlockModelElementResource bmer = face.getElement();
if (bmer.isRotation()){
if (element.getRotation().getAngle() > 0){
Quaternionf rot = Quaternionf.fromAngleDegAxis(
bmer.getRotationAngle(),
bmer.getRotationAxis().toVector().toFloat()
element.getRotation().getAngle(),
element.getRotation().getAxis().toVector().toFloat()
);
dir = rot.rotate(dir);
}
float a = dir.getY();
if (a > 0){
Texture t = resourcePack.getTextureProvider().getTexture(textureId);
if (t != null){
Vector4f c = t.getColor();
c = c.mul(color.toVector4(1f));
c = new Vector4f(c.getX(), c.getY(), c.getZ(), c.getW() * a);
model.mergeMapColor(c);
}
Vector4f c = texture.getColor();
c = c.mul(color.toVector4(1f));
c = new Vector4f(c.getX(), c.getY(), c.getZ(), c.getW() * a);
model.mergeMapColor(c);
}
}
private Block getRotationRelativeBlock(BlockModelResource model, Direction direction){
private Block getRotationRelativeBlock(TransformedBlockModelResource model, Direction direction){
return getRotationRelativeBlock(model, direction.toVector());
}
private Block getRotationRelativeBlock(BlockModelResource model, Vector3i direction){
private Block getRotationRelativeBlock(TransformedBlockModelResource model, Vector3i direction){
Vector3i dir = getRotationRelativeDirectionVector(model, direction.toFloat()).round().toInt();
return context.getRelativeBlock(dir);
}
private Vector3f getRotationRelativeDirectionVector(BlockModelResource model, Vector3f direction){
private Vector3f getRotationRelativeDirectionVector(TransformedBlockModelResource model, Vector3f direction){
Quaternionf rot = Quaternionf.fromAxesAnglesDeg(
-model.getXRot(),
-model.getYRot(),
-model.getRotation().getX(),
-model.getRotation().getY(),
0
);
Vector3f dir = rot.rotate(direction);
return dir;
}
private double testAo(BlockModelResource model, Vector3f vertex, Direction dir){
private double testAo(TransformedBlockModelResource model, Vector3f vertex, Direction dir){
int occluding = 0;
int x = 0;

View File

@ -0,0 +1,197 @@
/*
* 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.resourcepack;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import com.flowpowered.math.GenericMath;
import com.flowpowered.math.vector.Vector2f;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.render.context.BlockContext;
import de.bluecolored.bluemap.core.util.ConfigUtils;
import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.Block;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class BlockColorCalculator {
private BufferedImage foliageMap;
private BufferedImage grassMap;
private Map<String, Function<BlockContext, Vector3f>> blockColorMap;
public BlockColorCalculator(BufferedImage foliageMap, BufferedImage grassMap) {
this.foliageMap = foliageMap;
this.grassMap = grassMap;
this.blockColorMap = new HashMap<>();
}
public void loadColorConfig(File configFile) throws IOException {
blockColorMap.clear();
ConfigurationNode colorConfig = GsonConfigurationLoader.builder()
.setFile(configFile)
.build()
.load();
for (Entry<Object, ? extends ConfigurationNode> entry : colorConfig.getChildrenMap().entrySet()){
String key = entry.getKey().toString();
String value = entry.getValue().getString();
Function<BlockContext, Vector3f> colorFunction;
switch (value) {
case "@foliage":
colorFunction = this::getFoliageAverageColor;
break;
case "@grass":
colorFunction = this::getGrassAverageColor;
break;
case "@water":
colorFunction = this::getWaterAverageColor;
break;
default:
final Vector3f color = MathUtils.color3FromInt(ConfigUtils.readInt(entry.getValue()));
colorFunction = context -> color;
break;
}
blockColorMap.put(key, colorFunction);
}
}
public Vector3f getBlockColor(BlockContext context){
Block block = context.getRelativeBlock(0, 0, 0);
String blockId = block.getBlock().getFullId();
Function<BlockContext, Vector3f> colorFunction = blockColorMap.get(blockId);
if (colorFunction == null) colorFunction = blockColorMap.get("default");
if (colorFunction == null) colorFunction = this::getFoliageAverageColor;
return colorFunction.apply(context);
}
public Vector3f getWaterAverageColor(BlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(context.getRelativeBlock(x, 0, z).getBiome().getWaterColor());
}
}
return color.div(9f);
}
public Vector3f getFoliageAverageColor(BlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(getFoliageColor(context.getRelativeBlock(x, 0, z)));
}
}
return color.div(9f);
}
public Vector3f getFoliageColor(Block block){
int blocksAboveSeaLevel = Math.max(block.getPosition().getY() - block.getWorld().getSeaLevel(), 0);
return getFoliageColor(block.getBiome(), blocksAboveSeaLevel);
}
public Vector3f getFoliageColor(Biome biome, int blocksAboveSeaLevel){
Vector3f mapColor = getColorFromMap(biome, blocksAboveSeaLevel, foliageMap);
Vector3f overlayColor = biome.getOverlayFoliageColor().toVector3();
float overlayAlpha = biome.getOverlayFoliageColor().getW();
return mapColor.mul(1f - overlayAlpha).add(overlayColor.mul(overlayAlpha));
}
public Vector3f getGrassAverageColor(BlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(getGrassColor(context.getRelativeBlock(x, 0, z)));
}
}
return color.div(9f);
}
public Vector3f getGrassColor(Block block){
int blocksAboveSeaLevel = Math.max(block.getPosition().getY() - block.getWorld().getSeaLevel(), 0);
return getGrassColor(block.getBiome(), blocksAboveSeaLevel);
}
public Vector3f getGrassColor(Biome biome, int blocksAboveSeaLevel){
Vector3f mapColor = getColorFromMap(biome, blocksAboveSeaLevel, grassMap);
Vector3f overlayColor = biome.getOverlayGrassColor().toVector3();
float overlayAlpha = biome.getOverlayGrassColor().getW();
return mapColor.mul(1f - overlayAlpha).add(overlayColor.mul(overlayAlpha));
}
private Vector3f getColorFromMap(Biome biome, int blocksAboveSeaLevel, BufferedImage map){
Vector2i pixel = getColorMapPosition(biome, blocksAboveSeaLevel).mul(map.getWidth(), map.getHeight()).floor().toInt();
int cValue = map.getRGB(GenericMath.clamp(pixel.getX(), 0, map.getWidth() - 1), GenericMath.clamp(pixel.getY(), 0, map.getHeight() - 1));
Color color = new Color(cValue, false);
return new Vector3f(color.getRed(), color.getGreen(), color.getBlue()).div(0xff);
}
private Vector2f getColorMapPosition(Biome biome, int blocksAboveSeaLevel){
float adjTemp = (float) GenericMath.clamp(biome.getTemp() - (0.00166667 * blocksAboveSeaLevel), 0d, 1d);
float adjHumidity = (float) GenericMath.clamp(biome.getHumidity(), 0d, 1d) * adjTemp;
return new Vector2f(1 - adjTemp, 1 - adjHumidity);
}
public BufferedImage getFoliageMap() {
return foliageMap;
}
public void setFoliageMap(BufferedImage foliageMap) {
this.foliageMap = foliageMap;
}
public BufferedImage getGrassMap() {
return grassMap;
}
public void setGrassMap(BufferedImage grassMap) {
this.grassMap = grassMap;
}
}

View File

@ -1,239 +0,0 @@
/*
* 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.resourcepack;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
import com.flowpowered.math.GenericMath;
import com.flowpowered.math.vector.Vector2f;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.render.context.ExtendedBlockContext;
import de.bluecolored.bluemap.core.world.Block;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class BlockColorProvider {
private BufferedImage foliageMap;
private BufferedImage grassMap;
private Map<String, BiomeInfo> biomeInfos;
private Map<String, String> blockColors;
public BlockColorProvider(ResourcePack resourcePack) throws IOException, NoSuchResourceException {
this.foliageMap = ImageIO.read(resourcePack.getResource(Paths.get("assets", "minecraft", "textures", "colormap", "foliage.png")));
this.grassMap = ImageIO.read(resourcePack.getResource(Paths.get("assets", "minecraft", "textures", "colormap", "grass.png")));
this.biomeInfos = new ConcurrentHashMap<>();
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.setURL(getClass().getResource("/biomes.json"))
.build();
ConfigurationNode biomesConfig = loader.load();
for (Entry<Object, ? extends ConfigurationNode> n : biomesConfig.getChildrenMap().entrySet()){
String key = n.getKey().toString();
BiomeInfo value = new BiomeInfo();
value.humidity = n.getValue().getNode("humidity").getFloat(0.4f);
value.temp = n.getValue().getNode("temp").getFloat(0.8f);
value.watercolor = n.getValue().getNode("watercolor").getInt(4159204);
biomeInfos.put(key, value);
}
this.blockColors = new ConcurrentHashMap<>();
loader = GsonConfigurationLoader.builder()
.setURL(getClass().getResource("/blockColors.json"))
.build();
ConfigurationNode blockConfig = loader.load();
for (Entry<Object, ? extends ConfigurationNode> n : blockConfig.getChildrenMap().entrySet()){
String blockId = n.getKey().toString();
String color = n.getValue().getString();
blockColors.put(blockId, color);
}
}
public Vector3f getBlockColor(ExtendedBlockContext context){
Block block = context.getRelativeBlock(0, 0, 0);
String blockId = block.getBlock().getId();
// water color
if (blockId.equals("water")) {
return getBiomeWaterAverageColor(context);
}
String colorDef = blockColors.get(blockId);
if (colorDef == null) colorDef = blockColors.get("default");
if (colorDef == null) colorDef = "#foliage";
// grass map
if (colorDef.equals("#grass")){
return getBiomeGrassAverageColor(context);
}
// foliage map
if (colorDef.equals("#foliage")){
return getBiomeFoliageAverageColor(context);
}
int cValue = Integer.parseInt(colorDef, 16);
return colorFromInt(cValue);
}
public Vector3f getBiomeFoliageAverageColor(ExtendedBlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(getBiomeFoliageColor(context.getRelativeBlock(x, 0, z)));
}
}
return color.div(9f);
}
private Vector3f getBiomeFoliageColor(Block block){
Vector3f color = Vector3f.ONE;
if (block.getBiome().contains("mesa")){
return colorFromInt(0x9e814d);
}
if (block.getBiome().contains("swamp")) {
return colorFromInt(0x6A7039);
}
int blocksAboveSeaLevel = Math.max(block.getPosition().getY() - block.getWorld().getSeaLevel(), 0);
color = getFoliageColor(block.getBiome(), blocksAboveSeaLevel);
//improvised to match the original better
if (block.getBiome().contains("roofed_forest")){
color = color.mul(2f).add(colorFromInt(0x28340a)).div(3f);
}
return color;
}
public Vector3f getBiomeGrassAverageColor(ExtendedBlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(getBiomeGrassColor(context.getRelativeBlock(x, 0, z)));
}
}
return color.div(9f);
}
private Vector3f getBiomeGrassColor(Block block){
Vector3f color = Vector3f.ONE;
if (block.getBiome().contains("mesa")){
return colorFromInt(0x90814d);
}
if (block.getBiome().contains("swamp")) {
return colorFromInt(0x6A7039);
}
int blocksAboveSeaLevel = Math.max(block.getPosition().getY() - block.getWorld().getSeaLevel(), 0);
color = getGrassColor(block.getBiome(), blocksAboveSeaLevel);
if (block.getBiome().contains("roofed_forest")){
color = color.add(colorFromInt(0x28340a)).div(2f);
}
return color;
}
public Vector3f getBiomeWaterAverageColor(ExtendedBlockContext context){
Vector3f color = Vector3f.ZERO;
for (int x = -1; x <= 1; x++){
for (int z = -1; z <= 1; z++){
color = color.add(getBiomeWaterColor(context.getRelativeBlock(x, 0, z)));
}
}
return color.div(9f);
}
private Vector3f getBiomeWaterColor(Block block){
return colorFromInt(biomeInfos.get(block.getBiome()).watercolor);
}
private Vector3f colorFromInt(int cValue){
Color c = new Color(cValue, false);
return new Vector3f(c.getRed(), c.getGreen(), c.getBlue()).div(0xff);
}
private Vector3f getFoliageColor(String biomeId, int blocksAboveSeaLevel){
return getColorFromMap(biomeId, blocksAboveSeaLevel, foliageMap);
}
private Vector3f getGrassColor(String biomeId, int blocksAboveSeaLevel){
return getColorFromMap(biomeId, blocksAboveSeaLevel, grassMap);
}
private Vector3f getColorFromMap(String biomeId, int blocksAboveSeaLevel, BufferedImage map){
Vector2i pixel = getColorMapPosition(biomeId, blocksAboveSeaLevel).mul(map.getWidth(), map.getHeight()).floor().toInt();
int cValue = map.getRGB(GenericMath.clamp(pixel.getX(), 0, map.getWidth() - 1), GenericMath.clamp(pixel.getY(), 0, map.getHeight() - 1));
Color color = new Color(cValue, false);
return new Vector3f(color.getRed(), color.getGreen(), color.getBlue()).div(0xff);
}
private Vector2f getColorMapPosition(String biomeId, int blocksAboveSeaLevel){
BiomeInfo bi = biomeInfos.get(biomeId);
if (bi == null){
throw new NoSuchElementException("No biome found with id: " + biomeId);
}
float adjTemp = (float) GenericMath.clamp(bi.temp - (0.00166667 * blocksAboveSeaLevel), 0d, 1d);
float adjHumidity = (float) GenericMath.clamp(bi.humidity, 0d, 1d) * adjTemp;
return new Vector2f(1 - adjTemp, 1 - adjHumidity);
}
class BiomeInfo {
float humidity;
float temp;
int watercolor;
}
}

View File

@ -1,142 +0,0 @@
/*
* 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.resourcepack;
import com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.core.util.ConfigUtils;
import de.bluecolored.bluemap.core.util.Direction;
import ninja.leaping.configurate.ConfigurationNode;
public class BlockModelElementFaceResource {
private BlockModelElementResource element;
private Vector4f uv;
private String texture;
private String resolvedTexture;
private Direction cullface;
private int rotation;
private int tintIndex;
protected BlockModelElementFaceResource(BlockModelElementResource element, ConfigurationNode declaration) throws InvalidResourceDeclarationException {
this.element = element;
try {
this.uv = getDefaultUV(declaration.getKey().toString(), element.getFrom(), element.getTo());
ConfigurationNode uv = declaration.getNode("uv");
if (!uv.isVirtual()) this.uv = ConfigUtils.readVector4f(declaration.getNode("uv"));
this.texture = declaration.getNode("texture").getString();
this.resolvedTexture = null;
this.cullface = null;
ConfigurationNode cf = declaration.getNode("cullface");
if (!cf.isVirtual()) this.cullface = Direction.fromString(cf.getString());
this.rotation = declaration.getNode("rotation").getInt(0);
this.tintIndex = declaration.getNode("tintindex").getInt(-1);
} catch (NullPointerException | IllegalArgumentException e){
throw new InvalidResourceDeclarationException(e);
}
}
public Vector4f getDefaultUV(String faceId, Vector3f from, Vector3f to){
switch (faceId){
case "down" :
case "up" :
return new Vector4f(
from.getX(), from.getZ(),
to.getX(), to.getZ()
);
case "north" :
case "south" :
return new Vector4f(
from.getX(), from.getY(),
to.getX(), to.getY()
);
case "west" :
case "east" :
return new Vector4f(
from.getZ(), from.getY(),
to.getZ(), to.getY()
);
default :
return new Vector4f(
0, 0,
16, 16
);
}
}
public BlockModelElementResource getElement(){
return element;
}
public Vector4f getUv() {
return uv;
}
public String getTexture() {
return texture;
}
public String getResolvedTexture() {
if (resolvedTexture == null){
resolvedTexture = getElement().getModel().resolveTexture(getTexture());
}
return resolvedTexture;
}
public boolean isCullface() {
return cullface != null;
}
public Direction getCullface() {
return cullface;
}
public int getRotation() {
return rotation;
}
public boolean isTinted(){
return tintIndex >= 0;
}
public int getTintIndex() {
return tintIndex;
}
}

View File

@ -1,144 +0,0 @@
/*
* 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.resourcepack;
import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.util.Axis;
import de.bluecolored.bluemap.core.util.ConfigUtils;
import ninja.leaping.configurate.ConfigurationNode;
public class BlockModelElementResource {
private BlockModelResource model;
private Vector3f from, to;
private Vector3f rotOrigin;
private Axis rotAxis;
private float rotAngle;
private boolean rotRescale;
private boolean shade;
private BlockModelElementFaceResource down, up, north, south, west, east;
protected BlockModelElementResource(BlockModelResource model, ConfigurationNode declaration) throws InvalidResourceDeclarationException {
this.model = model;
try {
this.from = ConfigUtils.readVector3f(declaration.getNode("from"));
this.to = ConfigUtils.readVector3f(declaration.getNode("to"));
this.rotAngle = 0f;
ConfigurationNode rotation = declaration.getNode("rotation");
if (!rotation.isVirtual()){
this.rotOrigin = ConfigUtils.readVector3f(rotation.getNode("origin"));
this.rotAxis = Axis.fromString(rotation.getNode("axis").getString());
this.rotAngle = rotation.getNode("angle").getFloat();
this.rotRescale = rotation.getNode("rescale").getBoolean(false);
}
this.shade = declaration.getNode("shade").getBoolean(true);
ConfigurationNode faces = declaration.getNode("faces");
this.down = loadFace(faces.getNode("down"));
this.up = loadFace(faces.getNode("up"));
this.north = loadFace(faces.getNode("north"));
this.south = loadFace(faces.getNode("south"));
this.west = loadFace(faces.getNode("west"));
this.east = loadFace(faces.getNode("east"));
} catch (NullPointerException e){
throw new InvalidResourceDeclarationException(e);
}
}
private BlockModelElementFaceResource loadFace(ConfigurationNode faceNode) throws InvalidResourceDeclarationException {
if (faceNode.isVirtual()) return null;
return new BlockModelElementFaceResource(this, faceNode);
}
public BlockModelResource getModel(){
return model;
}
public Vector3f getFrom() {
return from;
}
public Vector3f getTo() {
return to;
}
public boolean isRotation(){
return rotAngle != 0f;
}
public Vector3f getRotationOrigin() {
return rotOrigin;
}
public Axis getRotationAxis() {
return rotAxis;
}
public float getRotationAngle() {
return rotAngle;
}
public boolean isRotationRescale() {
return rotRescale;
}
public boolean isShade() {
return shade;
}
public BlockModelElementFaceResource getDownFace() {
return down;
}
public BlockModelElementFaceResource getUpFace() {
return up;
}
public BlockModelElementFaceResource getNorthFace() {
return north;
}
public BlockModelElementFaceResource getSouthFace() {
return south;
}
public BlockModelElementFaceResource getWestFace() {
return west;
}
public BlockModelElementFaceResource getEastFace() {
return east;
}
}

View File

@ -25,118 +25,358 @@
package de.bluecolored.bluemap.core.resourcepack;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.NoSuchElementException;
import com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.BlockModelResource.Element.Face;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
import de.bluecolored.bluemap.core.util.Axis;
import de.bluecolored.bluemap.core.util.Direction;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class BlockModelResource {
private BlockStateResource blockState;
private ModelType modelType = ModelType.NORMAL;
private int xRot, yRot;
private boolean uvLock;
private boolean ambientOcclusion;
private Collection<BlockModelElementResource> elements;
private Map<String, String> textures;
private boolean ambientOcclusion = true;
private Collection<Element> elements = new ArrayList<>();
private Map<String, Texture> textures = new HashMap<>();
protected BlockModelResource(BlockStateResource blockState, ConfigurationNode declaration, ResourcePack resources) throws InvalidResourceDeclarationException {
this.blockState = blockState;
this.xRot = declaration.getNode("x").getInt(0);
this.yRot = declaration.getNode("y").getInt(0);
this.uvLock = declaration.getNode("uvlock").getBoolean(false);
this.ambientOcclusion = true;
this.elements = new Vector<>();
this.textures = new ConcurrentHashMap<>();
try {
loadModelResource(declaration.getNode("model").getString(), resources);
} catch (IOException e) {
throw new InvalidResourceDeclarationException("Model not found: " + declaration.getNode("model").getString(), e);
}
private BlockModelResource() {}
public ModelType getType() {
return modelType;
}
private void loadModelResource(String modelId, ResourcePack resources) throws IOException, InvalidResourceDeclarationException {
Path resourcePath = Paths.get("assets", "minecraft", "models", modelId + ".json");
ConfigurationNode data = GsonConfigurationLoader.builder()
.setSource(() -> new BufferedReader(new InputStreamReader(resources.getResource(resourcePath), StandardCharsets.UTF_8)))
.build()
.load();
//load parent first
ConfigurationNode parent = data.getNode("parent");
if (!parent.isVirtual()){
loadModelResource(parent.getString(), resources);
}
for (Entry<Object, ? extends ConfigurationNode> texture : data.getNode("textures").getChildrenMap().entrySet()){
String key = texture.getKey().toString();
String value = texture.getValue().getString();
textures.put(key, value);
}
ambientOcclusion = data.getNode("ambientocclusion").getBoolean(ambientOcclusion);
if (!data.getNode("elements").isVirtual()){
elements.clear();
for (ConfigurationNode e : data.getNode("elements").getChildrenList()){
elements.add(new BlockModelElementResource(this, e));
}
}
}
public BlockStateResource getBlockState(){
return blockState;
}
public int getXRot() {
return xRot;
}
public int getYRot() {
return yRot;
}
public boolean isUvLock() {
return uvLock;
}
public boolean isAmbientOcclusion() {
return ambientOcclusion;
}
public Collection<BlockModelElementResource> getElements() {
return Collections.unmodifiableCollection(elements);
public Collection<Element> getElements() {
return elements;
}
public String resolveTexture(String key){
if (key == null) return null;
if (!key.startsWith("#")) return key;
String texture = textures.get(key.substring(1));
if (texture == null) return key;
return resolveTexture(texture);
public Texture getTexture(String key) {
return textures.get(key);
}
public Collection<String> getAllTextureIds(){
List<String> list = new ArrayList<>();
for (String tex : textures.values()){
if (!tex.startsWith("#")) list.add(tex);
public class Element {
private Vector3f from = Vector3f.ZERO, to = new Vector3f(16f, 16f, 16f);
private Rotation rotation = new Rotation();
private boolean shade = true;
private EnumMap<Direction, Face> faces = new EnumMap<>(Direction.class);
private Element() {}
public Vector4f getDefaultUV(Direction face) {
switch (face){
case DOWN :
case UP :
return new Vector4f(
from.getX(), from.getZ(),
to.getX(), to.getZ()
);
case NORTH :
case SOUTH :
return new Vector4f(
from.getX(), from.getY(),
to.getX(), to.getY()
);
case WEST :
case EAST :
return new Vector4f(
from.getZ(), from.getY(),
to.getZ(), to.getY()
);
default :
return new Vector4f(
0, 0,
16, 16
);
}
}
return list;
public BlockModelResource getModel() {
return BlockModelResource.this;
}
public Vector3f getFrom() {
return from;
}
public Vector3f getTo() {
return to;
}
public Rotation getRotation() {
return rotation;
}
public boolean isShade() {
return shade;
}
public EnumMap<Direction, Face> getFaces() {
return faces;
}
public class Face {
private Vector4f uv;
private Texture texture;
private Direction cullface;
private int rotation = 0;
private boolean tinted = false;
private Face(Direction dir) {
uv = getDefaultUV(dir);
}
public Element getElement() {
return Element.this;
}
public Vector4f getUv() {
return uv;
}
public Texture getTexture() {
return texture;
}
public Direction getCullface() {
return cullface;
}
public int getRotation() {
return rotation;
}
public boolean isTinted() {
return tinted;
}
}
public class Rotation {
private Vector3f origin = new Vector3f(8, 8, 8);
private Axis axis = Axis.Y;
private float angle = 0;
private boolean rescale = false;
private Rotation() {}
public Vector3f getOrigin() {
return origin;
}
public Axis getAxis() {
return axis;
}
public float getAngle() {
return angle;
}
public boolean isRescale() {
return rescale;
}
}
}
public static Builder builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
return new Builder(sourcesAccess, resourcePack);
}
public static class Builder {
private FileAccess sourcesAccess;
private ResourcePack resourcePack;
private HashMap<String, String> textures;
private Builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
this.sourcesAccess = sourcesAccess;
this.resourcePack = resourcePack;
this.textures = new HashMap<>();
}
public synchronized BlockModelResource build(String modelPath) throws IOException, ParseResourceException {
textures.clear();
return buildNoReset(modelPath, true, modelPath);
}
private BlockModelResource buildNoReset(String modelPath, boolean renderElements, String topModelPath) throws IOException, ParseResourceException {
BlockModelResource blockModel = new BlockModelResource();
ConfigurationNode config = GsonConfigurationLoader.builder()
.setSource(() -> new BufferedReader(new InputStreamReader(sourcesAccess.readFile(modelPath), StandardCharsets.UTF_8)))
.build()
.load();
for (Entry<Object, ? extends ConfigurationNode> entry : config.getNode("textures").getChildrenMap().entrySet()) {
textures.putIfAbsent(entry.getKey().toString(), entry.getValue().getString(null));
}
String parentPath = config.getNode("parent").getString();
if (parentPath != null) {
if (parentPath.startsWith("builtin")) {
switch (parentPath) {
case "builtin/liquid":
blockModel.modelType = ModelType.LIQUID;
break;
}
} else {
try {
parentPath = ResourcePack.namespacedToAbsoluteResourcePath(parentPath, "models") + ".json";
blockModel = this.buildNoReset(parentPath, config.getNode("elements").isVirtual(), topModelPath);
} catch (IOException ex) {
Logger.global.logWarning("Failed to load parent model " + parentPath + " of model " + topModelPath + ": " + ex);
}
}
}
if (renderElements) {
for (ConfigurationNode elementNode : config.getNode("elements").getChildrenList()) {
try {
blockModel.elements.add(buildElement(blockModel, elementNode, topModelPath));
} catch (ParseResourceException ex) {
Logger.global.logWarning("Failed to parse element of model " + modelPath + " (" + topModelPath + "): " + ex);
}
}
}
for (String key : textures.keySet()) {
try {
blockModel.textures.put(key, getTexture("#" + key));
} catch (NoSuchElementException | FileNotFoundException ex) {
Logger.global.logDebug("Failed to map Texture key '" + key + "': " + ex);
}
}
return blockModel;
}
private Element buildElement(BlockModelResource model, ConfigurationNode node, String topModelPath) throws ParseResourceException {
Element element = model.new Element();
element.from = readVector3f(node.getNode("from"));
element.to = readVector3f(node.getNode("to"));
element.shade = node.getNode("shade").getBoolean(false);
if (!node.getNode("rotation").isVirtual()) {
element.rotation.angle = node.getNode("rotation", "angle").getFloat(0);
element.rotation.axis = Axis.fromString(node.getNode("rotation", "axis").getString("x"));
if (!node.getNode("rotation", "origin").isVirtual()) element.rotation.origin = readVector3f(node.getNode("rotation", "origin"));
element.rotation.rescale = node.getNode("rotation", "rescale").getBoolean(false);
}
for (Direction direction : Direction.values()) {
ConfigurationNode faceNode = node.getNode("faces", direction.name().toLowerCase());
if (!faceNode.isVirtual()) {
try {
Face face = buildFace(element, direction, faceNode);
element.faces.put(direction, face);
} catch (ParseResourceException | IOException ex) {
Logger.global.logDebug("Failed to parse an " + direction + " face for the model " + topModelPath + "! " + ex);
}
}
}
return element;
}
private Face buildFace(Element element, Direction direction, ConfigurationNode node) throws ParseResourceException, IOException {
try {
Face face = element.new Face(direction);
if (!node.getNode("uv").isVirtual()) face.uv = readVector4f(node.getNode("uv"));
face.texture = getTexture(node.getNode("texture").getString());
face.tinted = node.getNode("tintindex").getInt(-1) >= 0;
face.rotation = node.getNode("rotation").getInt(0);
if (!node.getNode("cullface").isVirtual()) {
String dirString = node.getNode("cullface").getString();
if (dirString.equals("bottom")) dirString = "down";
if (dirString.equals("top")) dirString = "up";
face.cullface = Direction.fromString(dirString);
}
return face;
} catch (FileNotFoundException ex) {
throw new ParseResourceException("There is no texture with the path: " + node.getNode("texture").getString(), ex);
} catch (NoSuchElementException ex) {
throw new ParseResourceException("Texture key '" + node.getNode("texture").getString() + "' has no texture assigned!", ex);
}
}
private Vector3f readVector3f(ConfigurationNode node) throws ParseResourceException {
List<? extends ConfigurationNode> nodeList = node.getChildrenList();
if (nodeList.size() < 3) throw new ParseResourceException("Failed to load Vector3: Not enough values in list-node!");
return new Vector3f(
nodeList.get(0).getFloat(0),
nodeList.get(1).getFloat(0),
nodeList.get(2).getFloat(0)
);
}
private Vector4f readVector4f(ConfigurationNode node) throws ParseResourceException {
List<? extends ConfigurationNode> nodeList = node.getChildrenList();
if (nodeList.size() < 4) throw new ParseResourceException("Failed to load Vector4: Not enough values in list-node!");
return new Vector4f(
nodeList.get(0).getFloat(0),
nodeList.get(1).getFloat(0),
nodeList.get(2).getFloat(0),
nodeList.get(3).getFloat(0)
);
}
private Texture getTexture(String key) throws NoSuchElementException, FileNotFoundException, IOException {
if (key.charAt(0) == '#') {
String value = textures.get(key.substring(1));
if (value == null) throw new NoSuchElementException("There is no texture defined for the key " + key);
return getTexture(value);
}
String path = ResourcePack.namespacedToAbsoluteResourcePath(key, "textures") + ".png";
Texture texture;
try {
texture = resourcePack.textures.get(path);
} catch (NoSuchElementException ex) {
texture = resourcePack.textures.loadTexture(sourcesAccess, path);
}
return texture;
}
}
}

View File

@ -28,127 +28,231 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
import de.bluecolored.bluemap.core.util.WeighedArrayList;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.world.BlockState;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class BlockStateResource {
private BlockState block;
private Collection<WeighedArrayList<BlockModelResource>> modelResources;
private List<Variant> variants = new ArrayList<>();
private Collection<Variant> multipart = new ArrayList<>();
protected BlockStateResource(BlockState block, ResourcePack resources) throws NoSuchResourceException, InvalidResourceDeclarationException {
this.block = Preconditions.checkNotNull(block);
this.modelResources = new Vector<>();
private BlockStateResource() {}
public Collection<TransformedBlockModelResource> getModels(BlockState blockState){
return getModels(blockState, Vector3i.ZERO);
}
public Collection<TransformedBlockModelResource> getModels(BlockState blockState, Vector3i pos){
Collection<TransformedBlockModelResource> models = new ArrayList<>();
for (Variant variant : variants) {
if (variant.condition.matches(blockState)) {
models.add(variant.getModel(pos));
return models;
}
}
try {
ConfigurationNode data = GsonConfigurationLoader.builder()
.setSource(() -> new BufferedReader(new InputStreamReader(resources.getResource(getResourcePath()), StandardCharsets.UTF_8)))
for (Variant variant : multipart) {
if (variant.condition.matches(blockState)) {
models.add(variant.getModel(pos));
}
}
return models;
}
private class Variant {
private PropertyCondition condition = PropertyCondition.all();
private Collection<Weighted<TransformedBlockModelResource>> models = new ArrayList<>();
private double totalWeight;
private Variant() {}
public TransformedBlockModelResource getModel(Vector3i pos) {
double selection = MathUtils.hashToFloat(pos, 827364) * totalWeight; //random based on position
for (Weighted<TransformedBlockModelResource> w : models) {
selection -= w.weight;
if (selection < 0) return w.value;
}
throw new RuntimeException("This line should never be reached!");
}
public void updateTotalWeight() {
totalWeight = 0d;
for (Weighted<?> w : models) {
totalWeight += w.weight;
}
}
}
private static class Weighted<T> {
private T value;
private double weight;
public Weighted(T value, double weight) {
this.value = value;
this.weight = weight;
}
}
public static Builder builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
return new Builder(sourcesAccess, resourcePack);
}
public static class Builder {
private final FileAccess sourcesAccess;
private final ResourcePack resourcePack;
private Builder(FileAccess sourcesAccess, ResourcePack resourcePack) {
this.sourcesAccess = sourcesAccess;
this.resourcePack = resourcePack;
}
public BlockStateResource build(String blockstateFile) throws IOException {
BlockStateResource blockState = new BlockStateResource();
ConfigurationNode config = GsonConfigurationLoader.builder()
.setSource(() -> new BufferedReader(new InputStreamReader(sourcesAccess.readFile(blockstateFile), StandardCharsets.UTF_8)))
.build()
.load();
load(data, resources);
} catch (IOException e) {
throw new NoSuchResourceException("There is no definition for resource-id: " + block.getId(), e);
} catch (NullPointerException e){
throw new InvalidResourceDeclarationException(e);
}
this.modelResources = Collections.unmodifiableCollection(this.modelResources);
}
private void load(ConfigurationNode data, ResourcePack resources) throws InvalidResourceDeclarationException {
//load variants
ConfigurationNode variants = data.getNode("variants");
for (Entry<Object, ? extends ConfigurationNode> e : variants.getChildrenMap().entrySet()){
if (getBlock().checkVariantCondition(e.getKey().toString())){
addModelResource(e.getValue(), resources);
break;
}
}
//load multipart
ConfigurationNode multipart = data.getNode("multipart");
for (ConfigurationNode part : multipart.getChildrenList()){
ConfigurationNode when = part.getNode("when");
if (when.isVirtual() || checkMultipartCondition(when)){
addModelResource(part.getNode("apply"), resources);
}
}
}
private void addModelResource(ConfigurationNode n, ResourcePack resources) throws InvalidResourceDeclarationException {
WeighedArrayList<BlockModelResource> models = new WeighedArrayList<>();
if (n.hasListChildren()){
//if it is a weighted list of alternative models, select one by random and weight
List<? extends ConfigurationNode> cList = n.getChildrenList();
for (ConfigurationNode c : cList){
int weight = c.getNode("weight").getInt(1);
models.add(new BlockModelResource(this, c, resources), weight);
//create variants
for (Entry<Object, ? extends ConfigurationNode> entry : config.getNode("variants").getChildrenMap().entrySet()) {
String conditionString = entry.getKey().toString();
ConfigurationNode transformedModelNode = entry.getValue();
Variant variant = blockState.new Variant();
variant.condition = parseConditionString(conditionString);
variant.models = loadModels(transformedModelNode, blockstateFile);
variant.updateTotalWeight();
blockState.variants.add(variant);
}
} else {
models.add(new BlockModelResource(this, n, resources));
}
modelResources.add(models);
}
private boolean checkMultipartCondition(ConfigurationNode when){
ConfigurationNode or = when.getNode("OR");
if (!or.isVirtual()){
for (ConfigurationNode condition : or.getChildrenList()){
if (checkMultipartCondition(condition)) return true;
//create multipart
for (ConfigurationNode partNode : config.getNode("multipart").getChildrenList()) {
Variant variant = blockState.new Variant();
ConfigurationNode whenNode = partNode.getNode("when");
if (!whenNode.isVirtual()) {
variant.condition = parseCondition(whenNode);
}
variant.models = loadModels(partNode.getNode("apply"), blockstateFile);
variant.updateTotalWeight();
blockState.multipart.add(variant);
}
return false;
return blockState;
}
Map<String, String> blockProperties = getBlock().getProperties();
for (Entry<Object, ? extends ConfigurationNode> e : when.getChildrenMap().entrySet()){
String key = e.getKey().toString();
String[] values = e.getValue().getString().split("\\|");
private Collection<Weighted<TransformedBlockModelResource>> loadModels(ConfigurationNode node, String blockstateFile) {
Collection<Weighted<TransformedBlockModelResource>> models = new ArrayList<>();
boolean found = false;
for (String value : values){
if (value.equals(blockProperties.get(key))){
found = true;
break;
if (node.hasListChildren()) {
for (ConfigurationNode modelNode : node.getChildrenList()) {
try {
models.add(loadModel(modelNode));
} catch (ParseResourceException ex) {
Logger.global.logWarning("Failed to load a model trying to parse " + blockstateFile + ": " + ex);
}
}
} else if (node.hasMapChildren()) {
try {
models.add(loadModel(node));
} catch (ParseResourceException ex) {
Logger.global.logWarning("Failed to load a model trying to parse " + blockstateFile + ": " + ex);
}
}
if (!found) return false;
return models;
}
return true;
}
private Weighted<TransformedBlockModelResource> loadModel(ConfigurationNode node) throws ParseResourceException {
String modelPath = node.getNode("model").getString();
if (modelPath == null) throw new ParseResourceException("No model defined!");
modelPath = ResourcePack.namespacedToAbsoluteResourcePath(modelPath, "models") + ".json";
BlockModelResource model = resourcePack.blockModelResources.get(modelPath);
if (model == null) {
try {
model = BlockModelResource.builder(sourcesAccess, resourcePack).build(modelPath);
} catch (IOException e) {
throw new ParseResourceException("Failed to load model " + modelPath, e);
}
resourcePack.blockModelResources.put(modelPath, model);
}
Vector2i rotation = new Vector2i(
node.getNode("x").getInt(0),
node.getNode("y").getInt(0)
);
boolean uvLock = node.getNode("uvlock").getBoolean(false);
TransformedBlockModelResource transformedModel = new TransformedBlockModelResource(rotation, uvLock, model);
return new Weighted<TransformedBlockModelResource>(transformedModel, node.getNode("weight").getDouble(1d));
}
public BlockState getBlock() {
return block;
private PropertyCondition parseCondition(ConfigurationNode conditionNode) {
List<PropertyCondition> andConditions = new ArrayList<>();
for (Entry<Object, ? extends ConfigurationNode> entry : conditionNode.getChildrenMap().entrySet()) {
String key = entry.getKey().toString();
if (key.equals("OR")) {
List<PropertyCondition> orConditions = new ArrayList<>();
for (ConfigurationNode orConditionNode : entry.getValue().getChildrenList()) {
orConditions.add(parseCondition(orConditionNode));
}
andConditions.add(PropertyCondition.or(orConditions.toArray(new PropertyCondition[orConditions.size()])));
} else {
String[] values = StringUtils.split(entry.getValue().getString(""), '|');
andConditions.add(PropertyCondition.property(key, values));
}
}
return PropertyCondition.and(andConditions.toArray(new PropertyCondition[andConditions.size()]));
}
private PropertyCondition parseConditionString(String conditionString) {
List<PropertyCondition> conditions = new ArrayList<>();
if (!conditionString.isEmpty() && !conditionString.equals("default")) {
String[] conditionSplit = StringUtils.split(conditionString, ',');
for (String element : conditionSplit) {
String[] keyval = StringUtils.split(element, "=", 2); //TODO what if it is wrong formatted?
conditions.add(PropertyCondition.property(keyval[0], keyval[1]));
}
}
PropertyCondition condition;
if (conditions.isEmpty()) {
condition = PropertyCondition.all();
} else if (conditions.size() == 1) {
condition = conditions.get(0);
} else {
condition = PropertyCondition.and(conditions.toArray(new PropertyCondition[conditions.size()]));
}
return condition;
}
}
public Collection<WeighedArrayList<BlockModelResource>> getModelResources(){
return modelResources;
}
private Path getResourcePath(){
return Paths.get("assets", block.getNamespace(), "blockstates", block.getId() + ".json");
}
}

View File

@ -24,20 +24,9 @@
*/
package de.bluecolored.bluemap.core.resourcepack;
public class InvalidResourceDeclarationException extends Exception {
private static final long serialVersionUID = 0L;
public enum ModelType {
public InvalidResourceDeclarationException() {}
NORMAL,
LIQUID;
public InvalidResourceDeclarationException(Throwable e) {
super(e);
}
public InvalidResourceDeclarationException(String message){
super(message);
}
public InvalidResourceDeclarationException(String message, Throwable e) {
super(message, e);
}
}

View File

@ -25,19 +25,18 @@
package de.bluecolored.bluemap.core.resourcepack;
public class NoSuchResourceException extends Exception {
private static final long serialVersionUID = 0L;
private static final long serialVersionUID = -8545428385061010089L;
public NoSuchResourceException() {}
public NoSuchResourceException(Throwable e) {
super(e);
public NoSuchResourceException() {
super();
}
public NoSuchResourceException(String message){
public NoSuchResourceException(String message) {
super(message);
}
public NoSuchResourceException(String message, Throwable e) {
super(message, e);
public NoSuchResourceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -24,20 +24,19 @@
*/
package de.bluecolored.bluemap.core.resourcepack;
public class NoSuchTextureException extends Exception {
private static final long serialVersionUID = 0L;
public class ParseResourceException extends Exception {
private static final long serialVersionUID = -2857915193389089307L;
public NoSuchTextureException() {}
public NoSuchTextureException(Throwable e) {
super(e);
public ParseResourceException() {
super();
}
public NoSuchTextureException(String message){
public ParseResourceException(String message) {
super(message);
}
public NoSuchTextureException(String message, Throwable e) {
super(message, e);
public ParseResourceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,137 @@
/*
* 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.resourcepack;
import com.google.common.base.Preconditions;
import de.bluecolored.bluemap.core.world.BlockState;
@FunctionalInterface
public interface PropertyCondition {
static final PropertyCondition MATCH_ALL = state -> true;
static final PropertyCondition MATCH_NONE = state -> false;
boolean matches(BlockState state);
public class Property implements PropertyCondition {
private String key;
private String value;
private Property (String key, String value) {
this.key = key.toLowerCase();
this.value = value.toLowerCase();
}
@Override
public boolean matches(BlockState state) {
String value = state.getProperties().get(this.key);
if (value == null) return false;
return value.equals(this.value);
}
}
class And implements PropertyCondition {
private PropertyCondition[] conditions;
private And (PropertyCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must be at least one condition!");
this.conditions = conditions;
}
@Override
public boolean matches(BlockState state) {
for (PropertyCondition condition : conditions) {
if (!condition.matches(state)) return false;
}
return true;
}
}
class Or implements PropertyCondition {
private PropertyCondition[] conditions;
private Or (PropertyCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must be at least one condition!");
this.conditions = conditions;
}
@Override
public boolean matches(BlockState state) {
for (PropertyCondition condition : conditions) {
if (condition.matches(state)) return true;
}
return false;
}
}
static PropertyCondition all() {
return MATCH_ALL;
}
static PropertyCondition none() {
return MATCH_NONE;
}
static PropertyCondition and(PropertyCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must be at least one condition!");
return new And(conditions);
}
static PropertyCondition or(PropertyCondition... conditions) {
Preconditions.checkArgument(conditions.length > 0, "Must be at least one condition!");
return new Or(conditions);
}
static PropertyCondition property(String key, String value) {
return new Property(key, value);
}
static PropertyCondition property(String key, String... possibleValues) {
Preconditions.checkArgument(possibleValues.length > 0, "Must be at least one value!");
if (possibleValues.length == 1) {
return property(key, possibleValues[0]);
}
PropertyCondition[] conditions = new PropertyCondition[possibleValues.length];
for (int i = 0; i < possibleValues.length; i++) {
conditions[i] = property(key, possibleValues[i]);
}
return or(conditions);
}
}

View File

@ -24,174 +24,175 @@
*/
package de.bluecolored.bluemap.core.resourcepack;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import javax.imageio.ImageIO;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.resourcepack.BlockStateResource.Builder;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.CombinedFileAccess;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
import de.bluecolored.bluemap.core.world.BlockState;
/**
* Represents all resources (BlockStates / BlockModels and Textures) that are loaded and used to generate map-models.
*/
public class ResourcePack {
public static final String MINECRAFT_CLIENT_VERSION = "1.14.4";
public static final String MINECRAFT_CLIENT_URL = "https://launcher.mojang.com/v1/objects/8c325a0c5bd674dd747d6ebaa4c791fd363ad8a9/client.jar";
private Map<Path, Resource> resources;
protected Map<String, BlockStateResource> blockStateResources;
protected Map<String, BlockModelResource> blockModelResources;
protected TextureGallery textures;
private TextureProvider textureProvider;
private BlockColorProvider blockColorProvider;
private Cache<BlockState, BlockStateResource> blockStateResourceCache;
private BlockColorCalculator blockColorCalculator;
public ResourcePack(List<File> dataSources, File textureExportFile) throws IOException, NoSuchResourceException {
this.resources = new HashMap<>();
//load resources in order
for (File resource : dataSources) overrideResourcesWith(resource);
blockStateResourceCache = CacheBuilder.newBuilder()
.maximumSize(10000)
.build();
textureProvider = new TextureProvider();
if (textureExportFile.exists()){
textureProvider.load(textureExportFile);
}
textureProvider.generate(this); //if loaded add missing textures
textureProvider.save(textureExportFile);
blockColorProvider = new BlockColorProvider(this);
private BufferedImage foliageMap;
private BufferedImage grassMap;
public ResourcePack() {
blockStateResources = new HashMap<>();
blockModelResources = new HashMap<>();
textures = new TextureGallery();
foliageMap = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
foliageMap.setRGB(0, 0, 0xFF00FF00);
grassMap = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
grassMap.setRGB(0, 0, 0xFF00FF00);
blockColorCalculator = new BlockColorCalculator(foliageMap, grassMap);
}
private void overrideResourcesWith(File resource){
if (resource.isFile() && resource.getName().endsWith(".zip") || resource.getName().endsWith(".jar")){
overrideResourcesWithZipFile(resource);
} else {
overrideResourcesWith(resource, Paths.get(""));
}
public void loadBlockColorConfig(File file) throws IOException {
blockColorCalculator.loadColorConfig(file);
}
private void overrideResourcesWith(File resource, Path resourcePath){
if (resource.isDirectory()){
for (File childFile : resource.listFiles()){
overrideResourcesWith(childFile, resourcePath.resolve(childFile.getName()));
}
return;
}
if (resource.isFile() && isActualResourcePath(resourcePath)){
try {
byte[] bytes = Files.readAllBytes(resource.toPath());
resources.put(resourcePath, new Resource(bytes));
} catch (IOException e) {
Logger.global.logError("Failed to load resource: " + resource, e);
}
}
/**
* See {@link TextureGallery#loadTextureFile(File)}
* @see TextureGallery#loadTextureFile(File)
*/
public void loadTextureFile(File file) throws IOException, ParseResourceException {
textures.loadTextureFile(file);
}
private void overrideResourcesWithZipFile(File resourceFile){
try (
ZipFile zipFile = new ZipFile(resourceFile);
){
Enumeration<? extends ZipEntry> files = zipFile.entries();
byte[] buffer = new byte[1024];
while (files.hasMoreElements()){
ZipEntry file = files.nextElement();
if (file.isDirectory()) continue;
Path resourcePath = Paths.get("", file.getName().split("/"));
if (!isActualResourcePath(resourcePath)) continue;
InputStream fileInputStream = zipFile.getInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream(Math.max(8, (int) file.getSize()));
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1){
bos.write(buffer, 0, bytesRead);
/**
* See {@link TextureGallery#saveTextureFile(File)}
* @see TextureGallery#saveTextureFile(File)
*/
public void saveTextureFile(File file) throws IOException {
textures.saveTextureFile(file);
}
/**
* Loads and generates all {@link BlockStateResource}s from the listed sources.
* Resources from sources that are "later" (more to the end) in the list are overriding resources from sources "earlier" (more to the start/head) in the list.<br>
* <br>
* Any exceptions occurred while loading the resources are logged and ignored.
*
* @param sources The list of {@link File} sources. Each can be a folder or any zip-compressed file. (E.g. .zip or .jar)
*/
public void load(Collection<File> sources) throws IOException {
load(sources.toArray(new File[sources.size()]));
}
/**
* Loads and generates all {@link BlockStateResource}s and {@link Texture}s from the listed sources.
* Resources from sources that are "later" (more to the end) in the list are overriding resources from sources "earlier" (more to the start/head) in the list.<br>
* <br>
* Any exceptions occurred while loading the resources are logged and ignored.
*
* @param sources The list of {@link File} sources. Each can be a folder or any zip-compressed file. (E.g. .zip or .jar)
*/
public void load(File... sources) {
try (CombinedFileAccess sourcesAccess = new CombinedFileAccess()){
for (File file : sources) {
try {
sourcesAccess.addFileAccess(FileAccess.of(file));
} catch (IOException e) {
Logger.global.logError("Failed to read ResourcePack: " + file, e);
}
resources.put(resourcePath, new Resource(bos.toByteArray()));
}
} catch (IOException e) {
Logger.global.logError("Failed to load resource: " + resourceFile, e);
}
}
private boolean isActualResourcePath(Path path) {
String[] blockstatesPattern = {"assets", ".*", "blockstates", "*"};
String[] modelsPattern = {"assets", ".*", "models", "blocks?", "*"};
String[] texturesPattern = {"assets", ".*", "textures", "block|colormap", "*"};
return
FileUtils.matchPath(path, blockstatesPattern) ||
FileUtils.matchPath(path, modelsPattern) ||
FileUtils.matchPath(path, texturesPattern);
}
public BlockStateResource getBlockStateResource(BlockState block) throws NoSuchResourceException, InvalidResourceDeclarationException {
BlockStateResource bsr = blockStateResourceCache.getIfPresent(block);
if (bsr == null){
bsr = new BlockStateResource(block, this);
blockStateResourceCache.put(block, bsr);
}
return bsr;
}
public TextureProvider getTextureProvider(){
return textureProvider;
}
public BlockColorProvider getBlockColorProvider(){
return blockColorProvider;
}
textures.reloadAllTextures(sourcesAccess);
Builder builder = BlockStateResource.builder(sourcesAccess, this);
Collection<String> namespaces = sourcesAccess.listFolders("assets");
for (String namespaceRoot : namespaces) {
String namespace = namespaceRoot.substring("assets/".length());
Collection<String> blockstateFiles = sourcesAccess.listFiles(namespaceRoot + "/blockstates", true);
for (String blockstateFile : blockstateFiles) {
String filename = FileAccess.getFileName(blockstateFile);
if (!filename.endsWith(".json")) continue;
public Map<Path, Resource> getAllResources() {
return Collections.unmodifiableMap(resources);
}
public InputStream getResource(Path resourcePath) throws NoSuchResourceException {
Resource resource = resources.get(resourcePath);
if (resource == null) throw new NoSuchResourceException("There is no resource with that path: " + resourcePath);
return resource.getStream();
}
public class Resource {
private byte[] data;
public Resource(byte[] data) {
this.data = data;
try {
blockStateResources.put(namespace + ":" + filename.substring(0, filename.length() - 5), builder.build(blockstateFile));
} catch (IOException ex) {
Logger.global.logError("Failed to load blockstate: " + namespace + ":" + filename.substring(0, filename.length() - 5), ex);
}
}
}
try {
foliageMap = ImageIO.read(sourcesAccess.readFile("assets/minecraft/textures/colormap/foliage.png"));
grassMap = ImageIO.read(sourcesAccess.readFile("assets/minecraft/textures/colormap/grass.png"));
blockColorCalculator.setFoliageMap(foliageMap);
blockColorCalculator.setGrassMap(grassMap);
} catch (IOException ex) {
Logger.global.logError("Failed to load foliage- or grass-map!", ex);
}
} catch (IOException ex) {
Logger.global.logError("Failed to close FileAccess!", ex);
}
public InputStream getStream(){
return new ByteArrayInputStream(data);
}
}
/**
* Returns a {@link BlockStateResource} for the given {@link BlockState} if found.
* @param state The {@link BlockState}
* @return The {@link BlockStateResource}
* @throws NoSuchResourceException If no resource is loaded for this {@link BlockState}
*/
public BlockStateResource getBlockStateResource(BlockState state) throws NoSuchResourceException {
BlockStateResource resource = blockStateResources.get(state.getFullId());
if (resource == null) throw new NoSuchResourceException("No resource for blockstate: " + state.getFullId());
return resource;
}
public BlockColorCalculator getBlockColorCalculator() {
return blockColorCalculator;
}
/**
* Synchronously downloads the default minecraft resources from the mojang-servers.
* @param file The file to save the downloaded resources to
* @throws IOException If an IOException occurs during the download
*/
public static void downloadDefaultResource(File file) throws IOException {
if (file.exists()) file.delete();
file.getParentFile().mkdirs();
org.apache.commons.io.FileUtils.copyURLToFile(new URL(MINECRAFT_CLIENT_URL), file, 10000, 10000);
}
protected static String namespacedToAbsoluteResourcePath(String namespacedPath, String resourceTypeFolder) {
String path = namespacedPath;
int namespaceIndex = path.indexOf(':');
String namespace = "minecraft";
if (namespaceIndex != -1) {
namespace = path.substring(0, namespaceIndex);
path = path.substring(namespaceIndex + 1);
}
path = "assets/" + namespace + "/" + resourceTypeFolder + "/" + FileAccess.normalize(path);
return path;
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.resourcepack;
import com.flowpowered.math.vector.Vector4f;
public class Texture {
private final int id;
private final String path;
private Vector4f color;
private boolean isHalfTransparent;
private String texture;
protected Texture(int id, String path, Vector4f color, boolean halfTransparent, String texture) {
this.id = id;
this.path = path;
this.color = color;
this.isHalfTransparent = halfTransparent;
this.texture = texture;
}
public int getId() {
return id;
}
public String getPath() {
return path;
}
/**
* Returns the calculated median color of the {@link Texture}.
* @return The median color of this {@link Texture}
*/
public Vector4f getColor() {
return color;
}
/**
* Returns whether the {@link Texture} has half-transparent pixels or not.
* @return <code>true</code> if the {@link Texture} has half-transparent pixels, <code>false</code> if not
*/
public boolean isHalfTransparent() {
return isHalfTransparent;
}
public String getTexture() {
return texture;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Texture) {
return ((Texture) obj).getId() == id;
}
return false;
}
}

View File

@ -0,0 +1,315 @@
/*
* 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.resourcepack;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.imageio.ImageIO;
import com.flowpowered.math.vector.Vector4f;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonStreamParser;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
/**
* A {@link TextureGallery} is managing {@link Texture}s and their id's and path's.<br>
* I can also load and generate the texture.json file, or load new {@link Texture}s from a {@link FileAccess}.
*/
public class TextureGallery {
private static final String EMPTY_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAEUlEQVR42mNkIAAYRxWMJAUAE5gAEdz4t9QAAAAASUVORK5CYII=";
private Map<String, Texture> textureMap;
private List<Texture> textureList;
public TextureGallery() {
textureMap = new HashMap<>();
textureList = new ArrayList<>();
}
/**
* Returns a {@link Texture} by its id, there can always be only one texture per id in a gallery.
* @param id The texture id
* @return The {@link Texture}
*/
public Texture get(int id) {
return textureList.get(id);
}
/**
* Returns a {@link Texture} by its path, there can always be only one texture per path in a gallery.
* @param path The texture-path
* @return The {@link Texture}
*/
public Texture get(String path) {
Texture texture = textureMap.get(path);
if (texture == null) throw new NoSuchElementException("There is no texture with the path " + path + " in this gallery!");
return texture;
}
/**
* The count of {@link Texture}s managed by this gallery
* @return The count of textures
*/
public int size() {
return textureList.size();
}
/**
* Generates a texture.json file with all the {@link Texture}s in this gallery
* @param file The file to save the json in
* @throws IOException If an IOException occurs while writing
*/
public void saveTextureFile(File file) throws IOException {
JsonArray textures = new JsonArray();
for (int i = 0; i < textureList.size(); i++) {
Texture texture = textureList.get(i);
JsonObject textureNode = new JsonObject();
textureNode.addProperty("id", texture.getPath());
textureNode.addProperty("texture", texture.getTexture());
textureNode.addProperty("transparent", texture.isHalfTransparent());
Vector4f color = texture.getColor();
JsonArray colorNode = new JsonArray();
colorNode.add(color.getX());
colorNode.add(color.getY());
colorNode.add(color.getZ());
colorNode.add(color.getW());
textureNode.add("color", colorNode);
textures.add(textureNode);
}
JsonObject root = new JsonObject();
root.add("textures", textures);
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.create();
String json = gson.toJson(root);
file.delete();
file.getParentFile().mkdirs();
file.createNewFile();
try (FileWriter fileWriter = new FileWriter(file)) {
fileWriter.append(json);
fileWriter.flush();
}
}
/**
* Loads all the {@link Texture}s from the provided texture.json file, removes any existing {@link Texture}s from this gallery.
* @param file The texture.json file.
* @throws IOException If an IOException occurs while reading the file.
* @throws ParseResourceException If the whole file can not be read. Errors with single textures are logged and ignored.
*/
public synchronized void loadTextureFile(File file) throws IOException, ParseResourceException {
textureList.clear();
textureMap.clear();
try (FileReader fileReader = new FileReader(file)){
JsonStreamParser jsonFile = new JsonStreamParser(fileReader);
JsonArray textures = jsonFile.next().getAsJsonObject().getAsJsonArray("textures");
int size = textures.size();
for (int i = 0; i < size; i++) {
while (i >= textureList.size()) { //prepopulate with placeholder so we don't get an IndexOutOfBounds below
textureList.add(new Texture(textureList.size(), "empty", Vector4f.ZERO, false, EMPTY_BASE64));
}
try {
JsonObject texture = textures.get(i).getAsJsonObject();
String path = texture.get("id").getAsString();
boolean transparent = texture.get("transparent").getAsBoolean();
Vector4f color = readVector4f(texture.get("color").getAsJsonArray());
textureList.set(i, new Texture(i, path, color, transparent, EMPTY_BASE64));
} catch (Exception ex) {
Logger.global.logWarning("Failed to load texture with id " + i + " from texture file " + file + "!");
}
}
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
throw new ParseResourceException("Invalid texture file format!", ex);
} finally {
regenerateMap();
}
}
/**
* Loads a {@link Texture} from the {@link FileAccess} and the given path and returns it.<br>
* If there is already a {@link Texture} with this path in this Gallery it replaces the {@link Texture} with the new one
* and the new one will have the same id as the old one.<br>
* Otherwise the {@link Texture} will be added to the end of this gallery with the next available id.
* @param fileAccess The {@link FileAccess} to load the image from.
* @param path The path of the image on the {@link FileAccess}
* @return The loaded {@link Texture}
* @throws FileNotFoundException If there is no image in that FileAccess on that path
* @throws IOException If an IOException occurred while loading the file
*/
public synchronized Texture loadTexture(FileAccess fileAccess, String path) throws FileNotFoundException, IOException {
try (InputStream input = fileAccess.readFile(path)) {
BufferedImage image = ImageIO.read(input);
if (image == null) throw new IOException("Failed to read image: " + path);
//crop off animation frames
if (image.getHeight() > image.getWidth()){
BufferedImage cropped = new BufferedImage(image.getWidth(), image.getWidth(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = cropped.createGraphics();
g.drawImage(image, 0, 0, null);
image = cropped;
}
//check halfTransparency
boolean halfTransparent = checkHalfTransparent(image);
//calculate color
Vector4f color = calculateColor(image);
//write to Base64
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, "png", os);
String base64 = "data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray());
//replace if texture with this path already exists
Texture texture = textureMap.get(path);
if (texture != null) {
texture = new Texture(texture.getId(), path, color, halfTransparent, base64);
textureMap.put(path, texture);
textureList.set(texture.getId(), texture);
} else {
texture = new Texture(textureList.size(), path, color, halfTransparent, base64);
textureMap.put(path, texture);
textureList.add(texture);
}
return texture;
}
}
/**
* Tries to reload all {@link Texture}s from the given {@link FileAccess}<br>
* <br>
* Exceptions are being logged and ignored.
* @param fileAccess The {@link FileAccess} to load the {@link Texture}s from
*/
public synchronized void reloadAllTextures(FileAccess fileAccess) {
for (Texture texture : textureList.toArray(new Texture[textureList.size()])) {
try {
loadTexture(fileAccess, texture.getPath());
} catch (IOException e) {
Logger.global.logWarning("Failed to reload texture: " + texture.getPath());
Logger.global.noFloodWarning("This happens if the resource-packs have changed, but you have not deleted your generated maps. This might result in broken map-models!");
}
}
}
private synchronized void regenerateMap() {
textureMap.clear();
for (int i = 0; i < textureList.size(); i++) {
Texture texture = textureList.get(i);
textureMap.put(texture.getPath(), texture);
}
}
private Vector4f readVector4f(JsonArray jsonArray) throws ParseResourceException {
if (jsonArray.size() < 4) throw new ParseResourceException("Failed to load Vector4: Not enough values in list-node!");
float r = jsonArray.get(0).getAsFloat();
float g = jsonArray.get(1).getAsFloat();
float b = jsonArray.get(2).getAsFloat();
float a = jsonArray.get(3).getAsFloat();
return new Vector4f(r, g, b, a);
}
private boolean checkHalfTransparent(BufferedImage image){
for (int x = 0; x < image.getWidth(); x++){
for (int y = 0; y < image.getHeight(); y++){
int pixel = image.getRGB(x, y);
int alpha = (pixel >> 24) & 0xff;
if (alpha > 0x00 && alpha < 0xff){
return true;
}
}
}
return false;
}
private Vector4f calculateColor(BufferedImage image){
double alpha = 0d, red = 0d, green = 0d, blue = 0d;
int count = 0;
for (int x = 0; x < image.getWidth(); x++){
for (int y = 0; y < image.getHeight(); y++){
int pixel = image.getRGB(x, y);
double pixelAlpha = (double)((pixel >> 24) & 0xff) / (double) 0xff;
double pixelRed = (double)((pixel >> 16) & 0xff) / (double) 0xff;
double pixelGreen = (double)((pixel >> 8) & 0xff) / (double) 0xff;
double pixelBlue = (double)((pixel >> 0) & 0xff) / (double) 0xff;
count++;
alpha += pixelAlpha;
red += pixelRed * pixelAlpha;
green += pixelGreen * pixelAlpha;
blue += pixelBlue * pixelAlpha;
}
}
if (count == 0 || alpha == 0) return Vector4f.ZERO;
red /= alpha;
green /= alpha;
blue /= alpha;
alpha /= count;
return new Vector4f(red, green, blue, alpha);
}
}

View File

@ -1,236 +0,0 @@
/*
* 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.resourcepack;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack.Resource;
import de.bluecolored.bluemap.core.util.ConfigUtils;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.util.MathUtils;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.gson.GsonConfigurationLoader;
public class TextureProvider {
private Map<String, Integer> indexMap;
private List<Texture> textures;
public TextureProvider() throws IOException {
this.indexMap = new ConcurrentHashMap<>();
this.textures = new Vector<>();
}
public int getTextureIndex(String textureId) throws NoSuchTextureException {
Integer tex = indexMap.get(textureId);
if (tex == null){
throw new NoSuchTextureException("There is no texture with id: " + textureId);
}
return tex.intValue();
}
public Texture getTexture(String textureId) throws NoSuchTextureException {
return getTexture(getTextureIndex(textureId));
}
public Texture getTexture(int index){
return textures.get(index);
}
public void generate(ResourcePack resources) throws IOException {
String[] texturesPathPattern = {"assets", ".*", "textures", "block", "*"};
for (Entry<Path, Resource> entry : resources.getAllResources().entrySet()){
Path key = entry.getKey();
if (FileUtils.matchPath(key, texturesPathPattern) && key.toString().endsWith(".png")){
String path = key.subpath(3, key.getNameCount()).normalize().toString();
String id = path
.substring(0, path.length() - ".png".length())
.replace(File.separatorChar, '/');
BufferedImage image = ImageIO.read(entry.getValue().getStream());
if (image == null) throw new IOException("Failed to read Image: " + key);
Texture texture = new Texture(id, image);
//update if existing else add new
if (indexMap.containsKey(id)) {
int index = indexMap.get(id);
textures.set(index, texture);
} else {
textures.add(texture);
indexMap.put(id, textures.size() - 1);
}
}
}
}
public void load(File file) throws IOException {
indexMap.clear();
textures.clear();
GsonConfigurationLoader loader = GsonConfigurationLoader.builder().setFile(file).build();
ConfigurationNode node = loader.load();
int i = 0;
for(ConfigurationNode n : node.getNode("textures").getChildrenList()){
Texture t = new Texture(
n.getNode("id").getString(),
n.getNode("texture").getString(),
n.getNode("transparent").getBoolean(false),
ConfigUtils.readVector4f(n.getNode("color"))
);
textures.add(t);
indexMap.put(t.getId(), i++);
}
}
public void save(File file) throws IOException {
if (!file.exists()) {
file.getParentFile().mkdirs();
file.createNewFile();
}
GsonConfigurationLoader loader = GsonConfigurationLoader.builder().setFile(file).build();
ConfigurationNode node = loader.createEmptyNode();
for (Texture t : textures){
ConfigurationNode n = node.getNode("textures").getAppendedNode();
n.getNode("id").setValue(t.getId());
n.getNode("texture").setValue(t.getBase64());
n.getNode("transparent").setValue(t.isHalfTransparent());
ConfigUtils.writeVector4f(n.getNode("color"), t.getColor());
}
loader.save(node);
}
public class Texture {
private String id;
private String base64;
private boolean halfTransparent;
private Vector4f color;
public Texture(String id, String base64, boolean halfTransparent, Vector4f color){
this.id = id;
this.halfTransparent = halfTransparent;
this.base64 = base64;
this.color = color;
}
public Texture(String id, BufferedImage image) throws IOException {
this.id = id;
//crop off animation frames
if (image.getHeight() > image.getWidth()){
BufferedImage cropped = new BufferedImage(image.getWidth(), image.getWidth(), image.getType());
Graphics2D g = cropped.createGraphics();
g.drawImage(image, 0, 0, null);
image = cropped;
}
//check halfTransparency
this.halfTransparent = checkHalfTransparent(image);
//calculate color
this.color = calculateColor(image);
//write to Base64
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, "png", os);
this.base64 = "data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray());
}
private Vector4f calculateColor(BufferedImage image){
Vector4f color = Vector4f.ZERO;
for (int x = 0; x < image.getWidth(); x++){
for (int y = 0; y < image.getHeight(); y++){
int pixel = image.getRGB(x, y);
double alpha = (double)((pixel >> 24) & 0xff) / (double) 0xff;
double red = (double)((pixel >> 16) & 0xff) / (double) 0xff;
double green = (double)((pixel >> 8) & 0xff) / (double) 0xff;
double blue = (double)((pixel >> 0) & 0xff) / (double) 0xff;
color = MathUtils.blendColors(new Vector4f(red, green, blue, alpha), color);
}
}
return color;
}
private boolean checkHalfTransparent(BufferedImage image){
for (int x = 0; x < image.getWidth(); x++){
for (int y = 0; y < image.getHeight(); y++){
int pixel = image.getRGB(x, y);
int alpha = (pixel >> 24) & 0xff;
if (alpha > 0x00 && alpha < 0xff){
return true;
}
}
}
return false;
}
public String getId() {
return id;
}
public String getBase64() {
return base64;
}
public boolean isHalfTransparent() {
return halfTransparent;
}
public Vector4f getColor(){
return color;
}
}
}

View File

@ -22,55 +22,33 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.util;
package de.bluecolored.bluemap.core.resourcepack;
import java.util.ArrayList;
import java.util.List;
import com.flowpowered.math.vector.Vector2i;
public class WeighedArrayList<E> extends ArrayList<E> implements List<E> {
private static final long serialVersionUID = 1L;
public class TransformedBlockModelResource {
private Vector2i rotation = Vector2i.ZERO;
private boolean uvLock = false;
public WeighedArrayList() {}
public WeighedArrayList(int capacity) {
super(capacity);
private BlockModelResource model;
public TransformedBlockModelResource(Vector2i rotation, boolean uvLock, BlockModelResource model) {
this.model = model;
this.rotation = rotation;
this.uvLock = uvLock;
}
/**
* Adds the element weight times to this list.
* @return Always true
*/
public void add(E e, int weight) {
for (int i = 0; i < weight; i++){
add(e);
}
public Vector2i getRotation() {
return rotation;
}
/**
* Removes the first weight number of items that equal o from this list.<br>
* @return The number of elements removed.
*/
public int remove(Object o, int weight) {
int removed = 0;
if (o == null){
for (int i = 0; i < size(); i++){
if (get(i) == null){
remove(i);
removed++;
if (removed >= weight) break;
}
}
} else {
for (int i = 0; i < size(); i++){
if (o.equals(get(i))){
remove(i);
removed++;
if (removed >= weight) break;
}
}
}
return removed;
public boolean isUVLock() {
return uvLock;
}
public BlockModelResource getModel() {
return model;
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.resourcepack.fileaccess;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CombinedFileAccess implements FileAccess {
public List<FileAccess> sources;
public CombinedFileAccess() {
sources = new ArrayList<>();
}
public void addFileAccess(FileAccess source) {
sources.add(source);
}
@Override
public InputStream readFile(String path) throws FileNotFoundException, IOException {
for (int i = sources.size() - 1; i >= 0; i--) { //reverse order because later sources override earlier ones
try {
return sources.get(i).readFile(path);
} catch (FileNotFoundException ex) {}
}
throw new FileNotFoundException("File " + path + " does not exist in any of the sources!");
}
@Override
public Collection<String> listFiles(String path, boolean recursive) {
Set<String> files = new HashSet<>();
for (int i = 0; i < sources.size(); i++) {
files.addAll(sources.get(i).listFiles(path, recursive));
}
return files;
}
@Override
public Collection<String> listFolders(String path) {
Set<String> folders = new HashSet<>();
for (int i = 0; i < sources.size(); i++) {
folders.addAll(sources.get(i).listFolders(path));
}
return folders;
}
@Override
public void close() throws IOException {
IOException exception = null;
for (FileAccess source : sources) {
try {
source.close();
} catch (IOException ex) {
if (exception == null) exception = ex;
else exception.addSuppressed(ex);
}
}
if (exception != null) throw exception;
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.resourcepack.fileaccess;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
public interface FileAccess extends Closeable, AutoCloseable {
InputStream readFile(String path) throws FileNotFoundException, IOException;
Collection<String> listFiles(String path, boolean recursive);
Collection<String> listFolders(String path);
static FileAccess of(File file) throws IOException {
if (file.isDirectory()) return new FolderFileAccess(file);
if (file.isFile()) return new ZipFileAccess(file);
throw new IOException("Unsupported file!");
}
static String getFileName(String path) {
String filename = path;
int nameSplit = path.lastIndexOf('/');
if (nameSplit > -1) {
filename = path.substring(nameSplit + 1);
}
return filename;
}
static String normalize(String path) {
if (path.charAt(path.length() - 1) == '/') path = path.substring(0, path.length() - 1);
if (path.charAt(0) == '/') path = path.substring(1);
return path;
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.resourcepack.fileaccess;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class FolderFileAccess implements FileAccess {
private File folder;
private Collection<WeakReference<InputStream>> openedStreams;
public FolderFileAccess(File folder) {
this.folder = folder;
openedStreams = new ArrayList<>();
}
@Override
public synchronized InputStream readFile(String path) throws FileNotFoundException {
InputStream stream = new FileInputStream(resolve(path).toFile());
tidy();
openedStreams.add(new WeakReference<>(stream));
return stream;
}
@Override
public Collection<String> listFiles(String path, boolean recursive) {
File subFolder = resolve(path).toFile();
List<String> paths = new ArrayList<>();
listFiles(subFolder, paths, recursive);
return paths;
}
private void listFiles(File folder, Collection<String> paths, boolean recursive) {
if (!folder.isDirectory()) return;
for (File file : folder.listFiles()) {
if (recursive && file.isDirectory()) listFiles(file, paths, true);
if (file.isFile()) paths.add(toPath(file));
}
}
@Override
public Collection<String> listFolders(String path) {
File subFolder = resolve(path).toFile();
List<String> paths = new ArrayList<>();
if (subFolder.isDirectory()) {
for (File file : subFolder.listFiles()) {
if (file.isDirectory()) paths.add(toPath(file));
}
}
return paths;
}
@Override
public synchronized void close() throws IOException {
IOException exception = null;
for (WeakReference<InputStream> streamRef : openedStreams) {
try {
InputStream stream = streamRef.get();
if (stream != null) stream.close();
streamRef.clear();
} catch (IOException ex) {
if (exception == null) exception = ex;
else exception.addSuppressed(ex);
}
}
if (exception != null) throw exception;
openedStreams.clear(); //only clear if no exception is thrown
}
private synchronized void tidy() {
Iterator<WeakReference<InputStream>> iterator = openedStreams.iterator();
while (iterator.hasNext()) {
WeakReference<InputStream> ref = iterator.next();
if (ref.get() == null) iterator.remove();
}
}
private Path resolve(String path) {
if (File.separatorChar != '/') path = path.replace('/', File.separatorChar);
if (path.charAt(0) == '/') path = path.substring(1);
Path resolve = folder.toPath();
for (String s : path.split("/")) {
resolve = resolve.resolve(s);
}
return resolve;
}
private String toPath(File file) {
return toPath(file.toPath());
}
private String toPath(Path path) {
return folder
.toPath()
.relativize(path)
.normalize()
.toString()
.replace(File.separatorChar, '/');
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.resourcepack.fileaccess;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
public class ZipFileAccess implements FileAccess {
private ZipFile file;
public ZipFileAccess(File file) throws ZipException, IOException {
this.file = new ZipFile(file);
}
@Override
public InputStream readFile(String path) throws FileNotFoundException, IOException {
ZipEntry entry = file.getEntry(path);
if (entry == null) throw new FileNotFoundException("File " + path + " does not exist in this zip-file!");
return file.getInputStream(entry);
}
@Override
public Collection<String> listFiles(String path, boolean recursive) {
path = normalizeFolderPath(path);
Collection<String> files = new ArrayList<String>();
for (Enumeration<? extends ZipEntry> entries = file.entries(); entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) continue;
String file = entry.getName();
int nameSplit = file.lastIndexOf('/');
String filePath = "";
if (nameSplit != -1) {
filePath = file.substring(0, nameSplit);
}
filePath = normalizeFolderPath(filePath);
if (recursive) {
if (!filePath.startsWith(path) && !path.equals(filePath)) continue;
} else {
if (!path.equals(filePath)) continue;
}
files.add(file);
}
return files;
}
@Override
public Collection<String> listFolders(String path) {
path = normalizeFolderPath(path);
Collection<String> folders = new ArrayList<String>();
for (Enumeration<? extends ZipEntry> entries = file.entries(); entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory()) continue;
String file = entry.getName();
file = file.substring(0, file.length() - 1); //strip last /
int nameSplit = file.lastIndexOf('/');
String filePath = "/";
if (nameSplit != -1) {
filePath = file.substring(0, nameSplit);
}
filePath = normalizeFolderPath(filePath);
if (!path.equals(filePath)) continue;
folders.add(file);
}
return folders;
}
private String normalizeFolderPath(String path) {
if (path.isEmpty()) return path;
if (path.charAt(path.length() - 1) != '/') path = path + "/";
if (path.charAt(0) == '/') path = path.substring(1);
return path;
}
@Override
public void close() throws IOException {
file.close();
}
}

View File

@ -132,4 +132,28 @@ public class ConfigUtils {
vectorNode.getAppendedNode().setValue(v.getW());
}
/**
* Returns an integer The value can be a normal integer, an integer in String-Format, or a string in hexadecimal format prefixed with #.
* @param node The Configuration Node with the value
* @return The parsed Integer
* @throws NumberFormatException If the value is not formatted correctly or if there is no value present.
*/
public static int readInt(ConfigurationNode node) throws NumberFormatException {
Object value = node.getValue();
if (value == null) throw new NumberFormatException("No value!");
if (value instanceof Number) {
return ((Number) value).intValue();
}
String val = value.toString();
if (val.charAt(0) == '#') {
return Integer.parseInt(val.substring(1), 16);
}
return Integer.parseInt(val);
}
}

View File

@ -33,6 +33,13 @@ public class MathUtils {
private MathUtils() {}
/**
* Calculates the surface-normal of a plane spanned between three vectors.
* @param p1 The first vector
* @param p2 The second vector
* @param p3 The third vector
* @return The calculated normal
*/
public static Vector3d getSurfaceNormal(Vector3d p1, Vector3d p2, Vector3d p3){
Vector3d u = p2.sub(p1);
Vector3d v = p3.sub(p1);
@ -44,6 +51,13 @@ public class MathUtils {
return new Vector3d(nX, nY, nZ);
}
/**
* Calculates the surface-normal of a plane spanned between three vectors.
* @param p1 The first vector
* @param p2 The second vector
* @param p3 The third vector
* @return The calculated normal
*/
public static Vector3f getSurfaceNormal(Vector3f p1, Vector3f p2, Vector3f p3) {
Vector3f u = p2.sub(p1);
Vector3f v = p3.sub(p1);
@ -58,18 +72,42 @@ public class MathUtils {
return n;
}
/**
* Hashes the provided position to a random float between 0 and 1.<br>
* <br>
* <i>(Implementation adapted from https://github.com/SpongePowered/SpongeAPI/blob/ecd761a70219e467dea47a09fc310e8238e9911f/src/main/java/org/spongepowered/api/extra/skylands/SkylandsUtil.java)</i>
*
* @param pos The position to hash
* @param seed A seed for the hashing
* @return The hashed value between 0 and 1
*/
public static float hashToFloat(Vector3i pos, long seed) {
return hashToFloat(pos.getX(), pos.getY(), pos.getZ(), seed);
}
/**
* Adapted from https://github.com/SpongePowered/SpongeAPI/blob/ecd761a70219e467dea47a09fc310e8238e9911f/src/main/java/org/spongepowered/api/extra/skylands/SkylandsUtil.java
* Hashes the provided position to a random float between 0 and 1.<br>
* <br>
* <i>(Implementation adapted from https://github.com/SpongePowered/SpongeAPI/blob/ecd761a70219e467dea47a09fc310e8238e9911f/src/main/java/org/spongepowered/api/extra/skylands/SkylandsUtil.java)</i>
*
* @param x The x component of the position
* @param y The y component of the position
* @param z The z component of the position
* @param seed A seed for the hashing
* @return The hashed value between 0 and 1
*/
public static float hashToFloat(int x, int y, int z, long seed) {
final long hash = x * 73428767 ^ y * 9122569 ^ z * 4382893 ^ seed * 457;
return (hash * (hash + 456149) & 0x00ffffff) / (float) 0x01000000;
}
/**
* Blends two colors, taking into account the alpha component
* @param top The top color
* @param bottom The bottom color
* @return The merged color
*/
public static Vector4f blendColors(Vector4f top, Vector4f bottom){
if (top.getW() > 0 && bottom.getW() > 0){
float a = 1 - (1 - top.getW()) * (1 - bottom.getW());
@ -84,6 +122,12 @@ public class MathUtils {
}
}
/**
* Overlays two colors, taking into account the alpha component
* @param top The top color
* @param bottom The bottom color
* @return The merged color
*/
public static Vector4f overlayColors(Vector4f top, Vector4f bottom){
if (top.getW() > 0 && bottom.getW() > 0){
float p = (1 - top.getW()) * bottom.getW();
@ -99,4 +143,31 @@ public class MathUtils {
}
}
/**
* Creates a {@link Vector3f} representing the color from the integer
* @param color The color-int
* @return The color-Vector
*/
public static Vector3f color3FromInt(int color){
return new Vector3f(
(color >> 16) & 0xFF,
(color >> 8) & 0xFF,
color & 0xFF
).div(0xFF);
}
/**
* Creates a {@link Vector4f} representing the color from the integer
* @param color The color-int
* @return The color-Vector
*/
public static Vector4f color4FromInt(int color){
return new Vector4f(
(color >> 16) & 0xFF,
(color >> 8) & 0xFF,
color & 0xFF,
(color >> 24) & 0xFF
).div(0xFF);
}
}

View File

@ -50,7 +50,7 @@ public class WebSettings {
.setFile(settingsFile)
.build();
load();
rootNode = configLoader.createEmptyNode();
}
public void load() throws IOException {

View File

@ -0,0 +1,108 @@
/*
* 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 com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.core.util.ConfigUtils;
import de.bluecolored.bluemap.core.util.MathUtils;
import ninja.leaping.configurate.ConfigurationNode;
public class Biome {
public static final Biome DEFAULT = new Biome();
private String id = "ocean";
private int ordinal = 0;
private float humidity = 0.5f;
private float temp = 0.5f;
private Vector3f waterColor = MathUtils.color3FromInt(4159204);
private Vector4f overlayFoliageColor = Vector4f.ZERO;
private Vector4f overlayGrassColor = Vector4f.ZERO;
private Biome() {}
public Biome(String id, int ordinal, float humidity, float temp, Vector3f waterColor) {
this.id = id;
this.ordinal = ordinal;
this.humidity = humidity;
this.temp = temp;
this.waterColor = waterColor;
}
public Biome(String id, int ordinal, float humidity, float temp, Vector3f waterColor, Vector4f overlayFoliageColor, Vector4f overlayGrassColor) {
this (id, ordinal, humidity, temp, waterColor);
this.overlayFoliageColor = overlayFoliageColor;
this.overlayGrassColor = overlayGrassColor;
}
public String getId() {
return id;
}
public int getOrdinal() {
return ordinal;
}
public float getHumidity() {
return humidity;
}
public float getTemp() {
return temp;
}
public Vector3f getWaterColor() {
return waterColor;
}
public Vector4f getOverlayFoliageColor() {
return overlayFoliageColor;
}
public Vector4f getOverlayGrassColor() {
return overlayGrassColor;
}
public static Biome create(String id, ConfigurationNode node) {
Biome biome = new Biome();
biome.id = id;
biome.ordinal = node.getNode("id").getInt(biome.ordinal);
biome.humidity = node.getNode("humidity").getFloat(biome.humidity);
biome.temp = node.getNode("temp").getFloat(biome.temp);
try { biome.waterColor = MathUtils.color3FromInt(ConfigUtils.readInt(node.getNode("watercolor"))); } catch (NumberFormatException ignored) {}
try { biome.overlayFoliageColor = MathUtils.color4FromInt(ConfigUtils.readInt(node.getNode("foliagecolor"))); } catch (NumberFormatException ignored) {}
try { biome.overlayGrassColor = MathUtils.color4FromInt(ConfigUtils.readInt(node.getNode("grasscolor"))); } catch (NumberFormatException ignored) {}
return biome;
}
}

View File

@ -56,7 +56,7 @@ public abstract class Block {
return isCullingNeighborFaces();
}
public abstract String getBiome();
public abstract Biome getBiome();
/**
* This is internally used for light rendering

View File

@ -51,6 +51,7 @@ public class BlockState {
private final String namespace;
private final String id;
private final String fullId;
private final Map<String, String> properties;
public BlockState(String id) {
@ -73,6 +74,7 @@ public class BlockState {
this.id = id;
this.namespace = namespace;
this.fullId = namespace + ":" + id;
}
private BlockState(BlockState blockState, String withKey, String withValue) {
@ -84,6 +86,7 @@ public class BlockState {
this.id = blockState.getId();
this.namespace = blockState.getNamespace();
this.fullId = namespace + ":" + id;
this.properties = Collections.unmodifiableMap(props);
}
@ -107,7 +110,7 @@ public class BlockState {
* Returns the namespaced id of this blockstate
*/
public String getFullId() {
return getNamespace() + ":" + getId();
return fullId;
}
/**

View File

@ -38,7 +38,7 @@ public class CachedBlock extends Block {
private World world;
private Vector3i position;
private double sunLight, blockLight;
private String biome;
private Biome biome;
private boolean isCullingCached;
private boolean isCulling;
@ -113,7 +113,7 @@ public class CachedBlock extends Block {
}
@Override
public String getBiome() {
public Biome getBiome() {
if (biome == null){
biome = block.getBiome();
}

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.mca.mapping;
package de.bluecolored.bluemap.core.world;
public class LightData {

View File

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

View File

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

View File

@ -0,0 +1,8 @@
{
"parent": "builtin/liquid",
"textures": {
"particle": "block/lava_still",
"still": "block/lava_still",
"flow": "block/lava_flow"
}
}

View File

@ -0,0 +1,8 @@
{
"parent": "builtin/liquid",
"textures": {
"particle": "block/water_still",
"still": "block/water_still",
"flow": "block/water_flow"
}
}

View File

@ -1,11 +1,12 @@
{
"default": "#foliage",
"grass_block" : "#grass",
"grass" : "#grass",
"tall_grass" : "#grass",
"fern" : "#grass",
"large_fern" : "#grass",
"redstone_wire" : "ff0000",
"birch_leaves" : "86a863",
"spruce_leaves" : "51946b"
"default": "@foliage",
"minecraft:water" : "@water",
"minecraft:grass_block" : "@grass",
"minecraft:grass" : "@grass",
"minecraft:tall_grass" : "@grass",
"minecraft:fern" : "@grass",
"minecraft:large_fern" : "@grass",
"minecraft:redstone_wire" : "#ff0000",
"minecraft:birch_leaves" : "#86a863",
"minecraft:spruce_leaves" : "#51946b"
}

Binary file not shown.

View File

@ -53,7 +53,17 @@ public class Slf4jLogger extends AbstractLogger {
@Override
public void logDebug(String message) {
out.debug(message);
if (out.isDebugEnabled()) out.debug(message);
}
@Override
public void noFloodDebug(String message) {
if (out.isDebugEnabled()) super.noFloodDebug(message);
}
@Override
public void noFloodDebug(String key, String message) {
if (out.isDebugEnabled()) super.noFloodDebug(key, message);
}
}

View File

@ -45,6 +45,7 @@ import java.util.zip.GZIPOutputStream;
import javax.inject.Inject;
import org.apache.commons.io.FileUtils;
import org.bstats.sponge.MetricsLite2;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.config.ConfigDir;
@ -68,7 +69,7 @@ import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.render.TileRenderer;
import de.bluecolored.bluemap.core.render.hires.HiresModelManager;
import de.bluecolored.bluemap.core.render.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.NoSuchResourceException;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.web.BlueMapWebServer;
import de.bluecolored.bluemap.core.web.WebFilesManager;
@ -125,7 +126,7 @@ public class SpongePlugin {
instance = this;
}
public synchronized void load() throws ExecutionException, IOException, NoSuchResourceException, InterruptedException {
public synchronized void load() throws ExecutionException, IOException, InterruptedException, ParseResourceException {
if (loaded) return;
unload(); //ensure nothing is left running (from a failed load or something)
@ -133,8 +134,14 @@ public class SpongePlugin {
File configFile = getConfigPath().resolve("bluemap.conf").toFile();
config = ConfigurationFile.loadOrCreate(configFile, SpongePlugin.class.getResource("/bluemap-sponge.conf")).getConfig();
File blockColorsConfigFile = getConfigPath().resolve("blockColors.json").toFile();
if (!blockColorsConfigFile.exists()) {
FileUtils.copyURLToFile(SpongePlugin.class.getResource("/blockColors.json"), blockColorsConfigFile, 10000, 10000);
}
//load resources
File defaultResourceFile = config.getDataPath().resolve("minecraft-client-" + ResourcePack.MINECRAFT_CLIENT_VERSION + ".jar").toFile();
File resourceExtensionsFile = config.getDataPath().resolve("resourceExtensions.zip").toFile();
File textureExportFile = config.getWebDataPath().resolve("textures.json").toFile();
if (!defaultResourceFile.exists()) {
@ -144,6 +151,9 @@ public class SpongePlugin {
Sponge.getCommandManager().register(this, new Commands(this).createStandaloneReloadCommand(), "bluemap");
return;
}
resourceExtensionsFile.delete();
FileUtils.copyURLToFile(SpongePlugin.class.getResource("/resourceExtensions.zip"), resourceExtensionsFile, 10000, 10000);
//find more resource packs
File resourcePackFolder = getConfigPath().resolve("resourcepacks").toFile();
@ -154,8 +164,13 @@ public class SpongePlugin {
List<File> resources = new ArrayList<>(resourcePacks.length + 1);
resources.add(defaultResourceFile);
for (File file : resourcePacks) resources.add(file);
resourcePack = new ResourcePack(resources, textureExportFile);
resources.add(resourceExtensionsFile);
resourcePack = new ResourcePack();
if (textureExportFile.exists()) resourcePack.loadTextureFile(textureExportFile);
resourcePack.load(resources);
resourcePack.loadBlockColorConfig(blockColorsConfigFile);
resourcePack.saveTextureFile(textureExportFile);
//load maps
for (MapConfig mapConfig : config.getMapConfigs()) {
@ -319,7 +334,7 @@ public class SpongePlugin {
loaded = false;
}
public synchronized void reload() throws IOException, NoSuchResourceException, ExecutionException, InterruptedException {
public synchronized void reload() throws IOException, ExecutionException, InterruptedException, ParseResourceException {
unload();
load();
}