493 lines
22 KiB
Java
493 lines
22 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.resources.resourcepack;
|
|
|
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
import com.github.benmanes.caffeine.cache.LoadingCache;
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonObject;
|
|
import de.bluecolored.bluemap.api.debug.DebugDump;
|
|
import de.bluecolored.bluemap.core.BlueMap;
|
|
import de.bluecolored.bluemap.core.logger.Logger;
|
|
import de.bluecolored.bluemap.core.resources.BlockColorCalculatorFactory;
|
|
import de.bluecolored.bluemap.core.resources.BlockPropertiesConfig;
|
|
import de.bluecolored.bluemap.core.resources.ResourcePath;
|
|
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
|
|
import de.bluecolored.bluemap.core.resources.biome.BiomeConfig;
|
|
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
|
|
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.TextureVariable;
|
|
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.BlockState;
|
|
import de.bluecolored.bluemap.core.resources.resourcepack.texture.AnimationMeta;
|
|
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
|
|
import de.bluecolored.bluemap.core.util.Tristate;
|
|
import de.bluecolored.bluemap.core.world.Biome;
|
|
import de.bluecolored.bluemap.core.world.BlockProperties;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import javax.imageio.ImageIO;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.Reader;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.FileSystem;
|
|
import java.nio.file.FileSystems;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionException;
|
|
import java.util.stream.Stream;
|
|
|
|
@DebugDump
|
|
public class ResourcePack {
|
|
public static final ResourcePath<BlockState> MISSING_BLOCK_STATE = new ResourcePath<>("bluemap", "missing");
|
|
public static final ResourcePath<BlockModel> MISSING_BLOCK_MODEL = new ResourcePath<>("bluemap", "block/missing");
|
|
public static final ResourcePath<Texture> MISSING_TEXTURE = new ResourcePath<>("bluemap", "block/missing");
|
|
|
|
private final Map<String, ResourcePath<BlockState>> blockStatePaths;
|
|
private final Map<ResourcePath<BlockState>, BlockState> blockStates;
|
|
private final Map<String, ResourcePath<BlockModel>> blockModelPaths;
|
|
private final Map<ResourcePath<BlockModel>, BlockModel> blockModels;
|
|
private final Map<String, ResourcePath<Texture>> texturePaths;
|
|
private final Map<ResourcePath<Texture>, Texture> textures;
|
|
private final Map<ResourcePath<BufferedImage>, BufferedImage> colormaps;
|
|
|
|
private final BlockColorCalculatorFactory colorCalculatorFactory;
|
|
private final BiomeConfig biomeConfig;
|
|
private final BlockPropertiesConfig blockPropertiesConfig;
|
|
|
|
private final LoadingCache<de.bluecolored.bluemap.core.world.BlockState, BlockProperties> blockPropertiesCache;
|
|
|
|
public ResourcePack() {
|
|
this.blockStatePaths = new HashMap<>();
|
|
this.blockStates = new HashMap<>();
|
|
this.blockModelPaths = new HashMap<>();
|
|
this.blockModels = new HashMap<>();
|
|
this.texturePaths = new HashMap<>();
|
|
this.textures = new HashMap<>();
|
|
this.colormaps = new HashMap<>();
|
|
|
|
this.colorCalculatorFactory = new BlockColorCalculatorFactory();
|
|
this.biomeConfig = new BiomeConfig();
|
|
this.blockPropertiesConfig = new BlockPropertiesConfig();
|
|
|
|
this.blockPropertiesCache = Caffeine.newBuilder()
|
|
.executor(BlueMap.THREAD_POOL)
|
|
.maximumSize(10000)
|
|
.build(this::loadBlockProperties);
|
|
}
|
|
|
|
@Nullable
|
|
public ResourcePath<BlockState> getBlockStatePath(String formatted) {
|
|
return blockStatePaths.get(formatted);
|
|
}
|
|
|
|
@Nullable
|
|
public BlockState getBlockState(de.bluecolored.bluemap.core.world.BlockState blockState) {
|
|
ResourcePath<BlockState> path = blockStatePaths.get(blockState.getFormatted());
|
|
return path != null ? path.getResource(this::getBlockState) : MISSING_BLOCK_STATE.getResource(this::getBlockState);
|
|
}
|
|
|
|
@Nullable
|
|
public BlockState getBlockState(ResourcePath<BlockState> path) {
|
|
BlockState blockState = blockStates.get(path);
|
|
return blockState != null ? blockState : MISSING_BLOCK_STATE.getResource(blockStates::get);
|
|
}
|
|
|
|
public Map<ResourcePath<BlockState>, BlockState> getBlockStates() {
|
|
return blockStates;
|
|
}
|
|
|
|
@Nullable
|
|
public ResourcePath<BlockModel> getBlockModelPath(String formatted) {
|
|
return blockModelPaths.get(formatted);
|
|
}
|
|
|
|
@Nullable
|
|
public BlockModel getBlockModel(ResourcePath<BlockModel> path) {
|
|
BlockModel blockModel = blockModels.get(path);
|
|
return blockModel != null ? blockModel : MISSING_BLOCK_MODEL.getResource(blockModels::get);
|
|
}
|
|
|
|
public Map<ResourcePath<BlockModel>, BlockModel> getBlockModels() {
|
|
return blockModels;
|
|
}
|
|
|
|
@Nullable
|
|
public ResourcePath<Texture> getTexturePath(String formatted) {
|
|
return texturePaths.get(formatted);
|
|
}
|
|
|
|
@Nullable
|
|
public Texture getTexture(ResourcePath<Texture> path) {
|
|
Texture texture = textures.get(path);
|
|
return texture != null ? texture : MISSING_TEXTURE.getResource(textures::get);
|
|
}
|
|
|
|
public Map<ResourcePath<Texture>, Texture> getTextures() {
|
|
return textures;
|
|
}
|
|
|
|
public BlockColorCalculatorFactory getColorCalculatorFactory() {
|
|
return colorCalculatorFactory;
|
|
}
|
|
|
|
public Biome getBiome(String formatted) {
|
|
return biomeConfig.getBiome(formatted);
|
|
}
|
|
|
|
public BlockProperties getBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
|
|
return blockPropertiesCache.get(state);
|
|
}
|
|
|
|
private BlockProperties loadBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
|
|
BlockProperties.Builder props = blockPropertiesConfig.getBlockProperties(state).toBuilder();
|
|
|
|
if (props.isOccluding() == Tristate.UNDEFINED || props.isCulling() == Tristate.UNDEFINED) {
|
|
BlockState resource = getBlockState(state);
|
|
if (resource != null) {
|
|
resource.forEach(state,0, 0, 0, variant -> {
|
|
BlockModel model = variant.getModel().getResource(this::getBlockModel);
|
|
if (model != null) {
|
|
if (props.isOccluding() == Tristate.UNDEFINED) props.occluding(model.isOccluding());
|
|
if (props.isCulling() == Tristate.UNDEFINED) props.culling(model.isCulling());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return props.build();
|
|
}
|
|
|
|
public synchronized void loadResources(Iterable<Path> roots) throws IOException, InterruptedException {
|
|
Logger.global.logInfo("Loading resources...");
|
|
|
|
for (Path root : roots) {
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
|
|
Logger.global.logDebug("Loading resources from: " + root + " ...");
|
|
loadResourcePath(root, this::loadResources);
|
|
}
|
|
|
|
Logger.global.logInfo("Loading textures...");
|
|
for (Path root : roots) {
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
|
|
Logger.global.logDebug("Loading textures from: " + root + " ...");
|
|
loadResourcePath(root, this::loadTextures);
|
|
}
|
|
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
|
|
Logger.global.logInfo("Baking resources...");
|
|
bake();
|
|
|
|
|
|
Logger.global.logInfo("Resources loaded.");
|
|
}
|
|
|
|
private void loadResourcePath(Path root, PathLoader resourceLoader) throws IOException, InterruptedException {
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
if (!Files.isDirectory(root)) {
|
|
try (FileSystem fileSystem = FileSystems.newFileSystem(root, (ClassLoader) null)) {
|
|
for (Path fsRoot : fileSystem.getRootDirectories()) {
|
|
if (!Files.isDirectory(fsRoot)) continue;
|
|
loadResourcePath(fsRoot, resourceLoader);
|
|
}
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to read '" + root + "': " + ex);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// load nested jars from fabric.mod.json if present
|
|
Path fabricModJson = root.resolve("fabric.mod.json");
|
|
if (Files.isRegularFile(fabricModJson)) {
|
|
try (BufferedReader reader = Files.newBufferedReader(fabricModJson)) {
|
|
JsonObject rootElement = ResourcesGson.INSTANCE.fromJson(reader, JsonObject.class);
|
|
if (rootElement.has("jars")) {
|
|
for (JsonElement element : rootElement.getAsJsonArray("jars")) {
|
|
Path file = root.resolve(element.getAsJsonObject().get("file").getAsString());
|
|
if (Files.exists(file)) loadResourcePath(file, resourceLoader);
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to read fabric.mod.json: " + ex);
|
|
}
|
|
}
|
|
|
|
resourceLoader.load(root);
|
|
}
|
|
|
|
private void loadResources(Path root) throws IOException {
|
|
try {
|
|
// do those in parallel
|
|
CompletableFuture.allOf(
|
|
|
|
// load blockstates
|
|
CompletableFuture.runAsync(() -> {
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("blockstates"))
|
|
.filter(Files::isDirectory)
|
|
.flatMap(ResourcePack::walk)
|
|
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> loadResource(root, file, () -> {
|
|
try (BufferedReader reader = Files.newBufferedReader(file)) {
|
|
return ResourcesGson.INSTANCE.fromJson(reader, BlockState.class);
|
|
}
|
|
}, blockStates));
|
|
}, BlueMap.THREAD_POOL),
|
|
|
|
// load blockmodels
|
|
CompletableFuture.runAsync(() -> {
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("models"))
|
|
.flatMap(ResourcePack::list)
|
|
.filter(path -> !path.getFileName().toString().equals("item"))
|
|
.flatMap(ResourcePack::walk)
|
|
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> loadResource(root, file, () -> {
|
|
try (BufferedReader reader = Files.newBufferedReader(file)) {
|
|
return ResourcesGson.INSTANCE.fromJson(reader, BlockModel.class);
|
|
}
|
|
}, blockModels));
|
|
}, BlueMap.THREAD_POOL),
|
|
|
|
// load colormaps
|
|
CompletableFuture.runAsync(() -> {
|
|
walk(root.resolve("assets").resolve("minecraft").resolve("textures").resolve("colormap"))
|
|
.filter(path -> path.getFileName().toString().endsWith(".png"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> loadResource(root, file, () -> {
|
|
try (InputStream in = Files.newInputStream(file)) {
|
|
return ImageIO.read(in);
|
|
}
|
|
}, colormaps));
|
|
}, BlueMap.THREAD_POOL),
|
|
|
|
// load block-color configs
|
|
CompletableFuture.runAsync(() -> {
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("blockColors.json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> {
|
|
try {
|
|
colorCalculatorFactory.load(file);
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
|
|
}
|
|
});
|
|
}, BlueMap.THREAD_POOL),
|
|
|
|
// load biome configs
|
|
// TODO: move this to datapacks?
|
|
CompletableFuture.runAsync(() -> {
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("biomes.json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> {
|
|
try {
|
|
biomeConfig.load(file);
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
|
|
}
|
|
});
|
|
|
|
list(root.resolve("data"))
|
|
.filter(Files::isDirectory)
|
|
.forEach(namespace -> list(namespace.resolve("worldgen").resolve("biome"))
|
|
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> {
|
|
try {
|
|
biomeConfig.loadDatapackBiome(namespace.getFileName().toString(), file);
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
|
|
}
|
|
})
|
|
);
|
|
}, BlueMap.THREAD_POOL),
|
|
|
|
// load block-properties configs
|
|
CompletableFuture.runAsync(() -> {
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("blockProperties.json"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> {
|
|
try {
|
|
blockPropertiesConfig.load(file);
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
|
|
}
|
|
});
|
|
}, BlueMap.THREAD_POOL)
|
|
|
|
).join();
|
|
|
|
} catch (RuntimeException ex) {
|
|
Throwable cause = ex.getCause();
|
|
if (cause instanceof IOException) throw (IOException) cause;
|
|
if (cause != null) throw new IOException(cause);
|
|
throw new IOException(ex);
|
|
}
|
|
}
|
|
|
|
private void loadTextures(Path root) throws IOException {
|
|
try {
|
|
|
|
// collect all used textures
|
|
Set<ResourcePath<Texture>> usedTextures = new HashSet<>();
|
|
usedTextures.add(MISSING_TEXTURE);
|
|
for (BlockModel model : blockModels.values()) {
|
|
for (TextureVariable textureVariable : model.getTextures().values()) {
|
|
if (textureVariable.isReference()) continue;
|
|
usedTextures.add(textureVariable.getTexturePath());
|
|
}
|
|
}
|
|
|
|
// load textures
|
|
list(root.resolve("assets"))
|
|
.map(path -> path.resolve("textures"))
|
|
.flatMap(ResourcePack::walk)
|
|
.filter(path -> path.getFileName().toString().endsWith(".png"))
|
|
.filter(Files::isRegularFile)
|
|
.forEach(file -> loadResource(root, file, () -> {
|
|
ResourcePath<Texture> resourcePath = new ResourcePath<>(root.relativize(file));
|
|
if (!usedTextures.contains(resourcePath)) return null; // don't load unused textures
|
|
|
|
// load image
|
|
BufferedImage image;
|
|
try (InputStream in = Files.newInputStream(file)) {
|
|
image = ImageIO.read(in);
|
|
}
|
|
|
|
// load animation
|
|
AnimationMeta animation = null;
|
|
Path animationPathFile = file.resolveSibling(file.getFileName() + ".mcmeta");
|
|
if (Files.exists(animationPathFile)) {
|
|
try (Reader in = Files.newBufferedReader(animationPathFile, StandardCharsets.UTF_8)) {
|
|
animation = ResourcesGson.INSTANCE.fromJson(in, AnimationMeta.class);
|
|
}
|
|
}
|
|
|
|
return Texture.from(resourcePath, image, animation);
|
|
|
|
}, textures));
|
|
|
|
} catch (RuntimeException ex) {
|
|
Throwable cause = ex.getCause();
|
|
if (cause instanceof IOException) throw (IOException) cause;
|
|
if (cause != null) throw new IOException(cause);
|
|
throw new IOException(ex);
|
|
}
|
|
}
|
|
|
|
private void bake() throws IOException, InterruptedException {
|
|
|
|
// fill path maps
|
|
blockStates.keySet().forEach(path -> blockStatePaths.put(path.getFormatted(), path));
|
|
blockModels.keySet().forEach(path -> blockModelPaths.put(path.getFormatted(), path));
|
|
textures.keySet().forEach(path -> texturePaths.put(path.getFormatted(), path));
|
|
|
|
// optimize references
|
|
for (BlockModel model : blockModels.values()) {
|
|
model.optimize(this);
|
|
}
|
|
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
|
|
// apply model parents
|
|
for (BlockModel model : blockModels.values()) {
|
|
model.applyParent(this);
|
|
}
|
|
|
|
if (Thread.interrupted()) throw new InterruptedException();
|
|
|
|
// calculate model properties
|
|
for (BlockModel model : blockModels.values()) {
|
|
model.calculateProperties(this);
|
|
}
|
|
|
|
BufferedImage foliage = new ResourcePath<BufferedImage>("minecraft:colormap/foliage").getResource(colormaps::get);
|
|
if (foliage == null) throw new IOException("Failed to bake resource-pack: No foliage-colormap found!");
|
|
this.colorCalculatorFactory.setFoliageMap(foliage);
|
|
|
|
BufferedImage grass = new ResourcePath<BufferedImage>("minecraft:colormap/grass").getResource(colormaps::get);
|
|
if (grass == null) throw new IOException("Failed to bake resource-pack: No grass-colormap found!");
|
|
this.colorCalculatorFactory.setGrassMap(grass);
|
|
|
|
}
|
|
|
|
private <T> void loadResource(Path root, Path file, Loader<T> loader, Map<ResourcePath<T>, T> resultMap) {
|
|
try {
|
|
ResourcePath<T> resourcePath = new ResourcePath<>(root.relativize(file));
|
|
if (resultMap.containsKey(resourcePath)) return; // don't load already present resources
|
|
|
|
T resource = loader.load();
|
|
if (resource == null) return; // don't load missing resources
|
|
|
|
resourcePath.setResource(resource);
|
|
resultMap.put(resourcePath, resource);
|
|
} catch (Exception ex) {
|
|
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
|
|
}
|
|
}
|
|
|
|
private static Stream<Path> list(Path root) {
|
|
if (!Files.isDirectory(root)) return Stream.empty();
|
|
try {
|
|
return Files.list(root);
|
|
} catch (IOException ex) {
|
|
throw new CompletionException(ex);
|
|
}
|
|
}
|
|
|
|
private static Stream<Path> walk(Path root) {
|
|
if (!Files.exists(root)) return Stream.empty();
|
|
if (Files.isRegularFile(root)) return Stream.of(root);
|
|
try {
|
|
return Files.walk(root);
|
|
} catch (IOException ex) {
|
|
throw new CompletionException(ex);
|
|
}
|
|
}
|
|
|
|
private interface Loader<T> {
|
|
T load() throws IOException;
|
|
}
|
|
|
|
private interface PathLoader {
|
|
void load(Path root) throws IOException;
|
|
}
|
|
|
|
}
|