BlueMap/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/resourcepack/ResourcePack.java

322 lines
14 KiB
Java

/*
* 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.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.blockmodel.BlockModelResource;
import de.bluecolored.bluemap.core.resourcepack.blockmodel.TransformedBlockModelResource;
import de.bluecolored.bluemap.core.resourcepack.blockstate.BlockStateResource;
import de.bluecolored.bluemap.core.resourcepack.blockstate.BlockStateResource.Builder;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.BluemapAssetOverrideFileAccess;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.CaseInsensitiveFileAccess;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.CombinedFileAccess;
import de.bluecolored.bluemap.core.resourcepack.fileaccess.FileAccess;
import de.bluecolored.bluemap.core.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resourcepack.texture.TextureGallery;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockProperties;
import de.bluecolored.bluemap.core.world.BlockState;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import javax.imageio.ImageIO;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Represents all resources (BlockStates / BlockModels and Textures) that are loaded and used to generate map-models.
*/
@DebugDump
public class ResourcePack {
private final Map<String, BlockStateResource> blockStateResources;
private final Map<String, BlockModelResource> blockModelResources;
private final TextureGallery textures;
private final BlockPropertiesConfig blockPropertiesConfig;
private final BiomeConfig biomeConfig;
private final BlockColorCalculatorFactory blockColorCalculatorFactory;
private final LoadingCache<BlockState, BlockProperties> blockPropertiesCache;
public ResourcePack() {
blockStateResources = new HashMap<>();
blockModelResources = new HashMap<>();
textures = new TextureGallery();
blockPropertiesConfig = new BlockPropertiesConfig();
biomeConfig = new BiomeConfig();
blockColorCalculatorFactory = new BlockColorCalculatorFactory();
blockPropertiesCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.maximumSize(10000)
.build(this::getBlockPropertiesNoCache);
}
/**
* 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, InterruptedException {
load(sources.toArray(new File[0]));
}
/**
* 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) throws InterruptedException {
try (CombinedFileAccess combinedSources = new CombinedFileAccess()){
for (File file : sources) {
if (Thread.interrupted()) throw new InterruptedException();
try {
combinedSources.addFileAccess(FileAccess.of(file));
} catch (IOException e) {
Logger.global.logError("Failed to read ResourcePack: " + file, e);
}
}
FileAccess sourcesAccess = new CaseInsensitiveFileAccess(new BluemapAssetOverrideFileAccess(combinedSources));
textures.reloadAllTextures(sourcesAccess);
Builder builder = BlockStateResource.builder(sourcesAccess, this);
Collection<String> namespaces = sourcesAccess.listFolders("assets");
int i = 0;
for (String namespaceRoot : namespaces) {
if (Thread.interrupted()) throw new InterruptedException();
i++;
//load blockstates
String namespace = namespaceRoot.substring("assets/".length());
Logger.global.logInfo("Loading " + namespace + " assets (" + i + "/" + namespaces.size() + ")...");
String blockstatesRootPath = namespaceRoot + "/blockstates";
Collection<String> blockstateFiles = sourcesAccess.listFiles(blockstatesRootPath, true);
for (String blockstateFile : blockstateFiles) {
if (Thread.interrupted()) throw new InterruptedException();
String filename = blockstateFile.substring(blockstatesRootPath.length() + 1);
if (!filename.endsWith(".json")) continue;
String jsonFileName = filename.substring(0, filename.length() - 5);
try {
blockStateResources.put(namespace + ":" + jsonFileName, builder.build(blockstateFile));
} catch (IOException ex) {
Logger.global.logError("Failed to load blockstate: " + namespace + ":" + jsonFileName, ex);
}
}
//load biomes
try {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.source(() -> new BufferedReader(new InputStreamReader(sourcesAccess.readFile(
"assets/" + namespace + "/biomes.json"))))
.build();
biomeConfig.load(loader.load());
} catch (IOException ex) {
Logger.global.logError("Failed to load biomes.conf from: " + namespace, ex);
}
//load block properties
try {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.source(() -> new BufferedReader(new InputStreamReader(sourcesAccess.readFile(
"assets/" + namespace + "/blockProperties.json"))))
.build();
blockPropertiesConfig.load(loader.load());
} catch (IOException ex) {
Logger.global.logError("Failed to load biomes.conf from: " + namespace, ex);
}
//load block colors
try {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.source(() -> new BufferedReader(new InputStreamReader(sourcesAccess.readFile(
"assets/" + namespace + "/blockColors.json"))))
.build();
blockColorCalculatorFactory.load(loader.load());
} catch (IOException ex) {
Logger.global.logError("Failed to load biomes.conf from: " + namespace, ex);
}
}
try {
blockColorCalculatorFactory.setFoliageMap(
ImageIO.read(sourcesAccess.readFile("assets/minecraft/textures/colormap/foliage.png"))
);
} catch (IOException | ArrayIndexOutOfBoundsException ex) {
Logger.global.logError("Failed to load foliagemap!", ex);
}
try {
blockColorCalculatorFactory.setGrassMap(
ImageIO.read(sourcesAccess.readFile("assets/minecraft/textures/colormap/grass.png"))
);
} catch (IOException | ArrayIndexOutOfBoundsException ex) {
Logger.global.logError("Failed to load grassmap!", ex);
}
} catch (IOException ex) {
Logger.global.logError("Failed to close FileAccess!", ex);
}
}
/**
* See {@link TextureGallery#loadTextureFile(File)}
* @see TextureGallery#loadTextureFile(File)
*/
public void loadTextureFile(File file) throws IOException, ParseResourceException {
textures.loadTextureFile(file);
}
/**
* See {@link TextureGallery#saveTextureFile(File)}
* @see TextureGallery#saveTextureFile(File)
*/
public void saveTextureFile(File file) throws IOException {
textures.saveTextureFile(file);
}
/**
* 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 BlockProperties getBlockProperties(BlockState state) {
return blockPropertiesCache.get(state);
}
private BlockProperties getBlockPropertiesNoCache(BlockState state) {
BlockProperties.Builder props = blockPropertiesConfig.getBlockProperties(state).toBuilder();
if (props.isOccluding() == Tristate.UNDEFINED || props.isCulling() == Tristate.UNDEFINED) {
try {
BlockStateResource resource = getBlockStateResource(state);
for (TransformedBlockModelResource bmr : resource.getModels(state, new ArrayList<>())) {
if (props.isOccluding() == Tristate.UNDEFINED) props.occluding(bmr.getModel().isOccluding());
if (props.isCulling() == Tristate.UNDEFINED) props.culling(bmr.getModel().isCulling());
}
} catch (NoSuchResourceException ignore) {}
}
return props.build();
}
public Biome getBiome(String id) {
return biomeConfig.getBiome(id);
}
public Map<String, BlockStateResource> getBlockStateResources() {
return blockStateResources;
}
public Map<String, BlockModelResource> getBlockModelResources() {
return blockModelResources;
}
public TextureGallery getTextures() {
return textures;
}
public BlockColorCalculatorFactory getBlockColorCalculatorFactory() {
return blockColorCalculatorFactory;
}
public static String namespacedToAbsoluteResourcePath(String namespacedPath, String resourceTypeFolder) {
String path = namespacedPath;
resourceTypeFolder = FileAccess.normalize(resourceTypeFolder);
int namespaceIndex = path.indexOf(':');
String namespace = "minecraft";
if (namespaceIndex != -1) {
namespace = path.substring(0, namespaceIndex);
path = path.substring(namespaceIndex + 1);
}
if (resourceTypeFolder.isEmpty()) {
path = "assets/" + namespace + "/" + FileAccess.normalize(path);
} else {
path = "assets/" + namespace + "/" + resourceTypeFolder + "/" + FileAccess.normalize(path);
}
return path;
}
/**
* Caches a full InputStream in a byte-array that can be read later
*/
public static class Resource {
private final byte[] data;
public Resource(InputStream data) throws IOException {
try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
bout.write(data);
this.data = bout.toByteArray();
} finally {
data.close();
}
}
/**
* Creates a new InputStream to read this resource
*/
public InputStream read() {
return new ByteArrayInputStream(this.data);
}
}
}