Skip to content

Commit

Permalink
Refactor resource pack loading (#1734)
Browse files Browse the repository at this point in the history
* Refactor resource packs loading to support loading arbitrary files from a resourcepack.

For now, this makes loading painting variant and biome registries less hacky. In the future, this enables loading other files, eg. model json files or PBR maps.

* Apply single color textures without a restart by making Texture.useAverageColor static.
  • Loading branch information
leMaik authored Jul 6, 2024
1 parent dac29e8 commit ee088a7
Show file tree
Hide file tree
Showing 33 changed files with 404 additions and 249 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public int[] getData() {
}
@Override
public float[] getColor(int x, int y) {
if(empty.usesAverageColor())
if(useAverageColor)
return empty.getAvgColorFlat();
float[] result = new float[4];
boolean shouldOverlay = bookPresentAt(x, y);
Expand Down
141 changes: 141 additions & 0 deletions chunky/src/java/se/llbit/chunky/resources/LayeredResourcePacks.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package se.llbit.chunky.resources;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class LayeredResourcePacks implements Closeable {
private final List<ResourcePack> resourcePacks = new ArrayList<>();

public void addResourcePack(File resourcePackFile) throws IOException {
this.resourcePacks.add(new ResourcePack(resourcePackFile));
}

public List<File> getResourcePackFiles() {
return Collections.unmodifiableList(resourcePacks.stream().map(ResourcePack::getFile).collect(Collectors.toList()));
}

public Optional<InputStream> getInputStream(String path) throws IOException {
Optional<Entry> entry = getFirstEntry(path);
if (entry.isPresent()) {
return Optional.ofNullable(entry.get().getInputStream());
}
return Optional.empty();
}

public Optional<Entry> getFirstEntry(String path) {
for (ResourcePack pack : resourcePacks) {
try {
Path resolvedPath = pack.getRootPath().resolve(path);
if (Files.exists(resolvedPath)) {
return Optional.of(new Entry(pack, resolvedPath));
}
} catch (IOException e) {
// ignore
}
}
return Optional.empty();
}

public Iterable<Entry> getAllEntries(String path) {
List<Entry> entries = new ArrayList<>();
for (ResourcePack pack : resourcePacks) {
Path resolvedPath;
try {
resolvedPath = pack.getRootPath().resolve(path);
} catch (IOException e) {
continue;
}
if (Files.exists(resolvedPath)) {
entries.add(new Entry(pack, resolvedPath));
}
}
return entries;
}

@Override
public void close() throws IOException {
for (ResourcePack resourcePack : resourcePacks) {
resourcePack.close();
}
}

public static class ResourcePack implements Closeable {
public File file;
private FileSystem fileSystem;
private Path rootPath;

private ResourcePack(File resourcePackFile) {
this.file = resourcePackFile;
}

public File getFile() {
return file;
}

private FileSystem getFileSystem() throws IOException {
if (fileSystem != null && fileSystem.isOpen()) {
return fileSystem;
}
return fileSystem = ResourcePackLoader.getPackFileSystem(file);
}

public Path getRootPath() throws IOException {
if (rootPath == null) {
Path rootPath = ResourcePackLoader.getPackRootPath(file, getFileSystem());
String baseName = file.getName();
if (baseName.toLowerCase().endsWith(".zip")) {
// The assets directory can be inside a top-level directory with
// the same name as the resource pack zip file.
baseName = baseName.substring(0, baseName.length() - 4);
if (Files.exists(rootPath.resolve(baseName).resolve("assets"))) {
rootPath = rootPath.resolve(baseName);
}
}
this.rootPath = rootPath;
}

return rootPath;
}

@Override
public void close() throws IOException {
try {
fileSystem.close();
} catch (UnsupportedOperationException e) {
// default file systems do not support closing
}
}
}

public static class Entry {
private final ResourcePack pack;
private final Path path;

public Entry(ResourcePack pack, Path path) {
this.pack = pack;
this.path = path;
}

public ResourcePack getPack() {
return pack;
}

public Path getPath() {
return path;
}

public InputStream getInputStream() throws IOException {
return Files.newInputStream(path);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import se.llbit.chunky.world.biome.Biomes;
import se.llbit.log.Log;

import java.io.*;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
Expand All @@ -33,7 +34,8 @@
import java.util.stream.Stream;

public class ResourcePackBiomeLoader implements ResourcePackLoader.PackLoader {
public ResourcePackBiomeLoader() {}
public ResourcePackBiomeLoader() {
}

protected static final Gson GSON = new GsonBuilder()
.disableJdkUnsafe()
Expand All @@ -54,10 +56,9 @@ protected static class BiomeEffects {
}

@Override
public boolean load(Path pack, String baseName) {
Path data = pack.resolve("data");
if (Files.exists(data)) {
try (Stream<Path> namespaces = Files.list(data)) {
public boolean load(LayeredResourcePacks resourcePacks) {
for (LayeredResourcePacks.Entry data : resourcePacks.getAllEntries("data")) {
try (Stream<Path> namespaces = Files.list(data.getPath())) {
namespaces.forEach(ns -> {
String namespace = String.valueOf(ns.getFileName());

Expand Down
127 changes: 52 additions & 75 deletions chunky/src/java/se/llbit/chunky/resources/ResourcePackLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.*;
Expand All @@ -46,7 +47,7 @@ public interface PackLoader {
* @return True if this loader has found and loaded _all_ the things it is responsible for.
* False if there is more to load (by a fallback resource pack).
*/
boolean load(Path pack, String baseName);
boolean load(LayeredResourcePacks resourcePacks);

/**
* All resources that failed to load. Empty if all resources were loaded.
Expand All @@ -64,13 +65,13 @@ interface PackLoaderFactory {
PackLoader create();
}

private static List<File> loadedResourcePacks = Collections.emptyList();
private static LayeredResourcePacks resourcePacks = new LayeredResourcePacks();

/**
* @return loaded resource packs without default pack
* @return loaded resource packs
*/
public static List<File> getLoadedResourcePacks() {
return Collections.unmodifiableList(loadedResourcePacks);
return resourcePacks.getResourcePackFiles();
}

public static List<File> getAvailableResourcePacks() {
Expand Down Expand Up @@ -119,107 +120,80 @@ public static void loadResourcePacks(List<File> resourcePacks) {
TextureCache.reset();
Biomes.reset();

loadedResourcePacks = resourcePacks.stream()
.distinct()
.collect(Collectors.toList());

List<PackLoader> loaders = PACK_LOADER_FACTORIES.stream()
.map(PackLoaderFactory::create)
.collect(Collectors.toList());

if (!reloadResourcePacks(loaders)) {
Log.info(buildMissingResourcesErrorMessage(loaders));
if (ResourcePackLoader.resourcePacks != null) {
try {
ResourcePackLoader.resourcePacks.close();
} catch (IOException e) {
// ignore
}
}
}

/**
* Load specific resources from the currently used set of resource packs.
*
* @return True if all resources have been found and loaded.
*/
public static boolean loadResources(PackLoader... loaders) {
return reloadResourcePacks(Arrays.asList(loaders));
}

private static boolean reloadResourcePacks(List<PackLoader> loaders) {
List<File> resourcePacks = new ArrayList<>(getLoadedResourcePacks());

LayeredResourcePacks packs = new LayeredResourcePacks();
for (File packFile : resourcePacks) {
try {
packs.addResourcePack(packFile);
} catch (IOException e) {
Log.warnf(
"Failed to open %s (%s): %s",
getResourcePackDescriptor(packFile),
packFile.getAbsolutePath(),
e.getMessage()
);
}
}
if (!PersistentSettings.getDisableDefaultTextures()) {
File file = MinecraftFinder.getMinecraftJar();
if (file != null) {
resourcePacks.add(file);
try {
packs.addResourcePack(file);
} catch (IOException e) {
Log.warn("Minecraft Jar could not be opened: falling back to placeholder textures.");
}
} else {
Log.warn("Minecraft Jar not found: falling back to placeholder textures.");
}
}
ResourcePackLoader.resourcePacks = packs;

return loadResourcePacks(resourcePacks, loaders);
}

private static boolean loadResourcePacks(List<File> resourcePacks, List<PackLoader> loaders) {
Log.infof(
"Loading resource packs: \n%s",
resourcePacks.stream()
.map(File::toString)
.map(s -> "- " + s)
.collect(Collectors.joining("\n"))
);
return loadResourcePacks(
resourcePacks.iterator(),
loaders
);

List<PackLoader> loaders = PACK_LOADER_FACTORIES.stream()
.map(PackLoaderFactory::create)
.collect(Collectors.toList());

if (!loadResources(loaders)) {
Log.info(buildMissingResourcesErrorMessage(loaders));
}
}

/**
* Load resources from the given resource packs.
* Resource pack files are loaded in list order - if a texture is not found in a pack,
* the next packs is checked as a fallback.
* Load specific resources from the currently used set of resource packs.
*
* @return True if all resources have been found and loaded.
*/
private static boolean loadResourcePacks(Iterator<File> resourcePacks, List<PackLoader> loaders) {
while (resourcePacks.hasNext()) {
File resourcePack = resourcePacks.next();
if (resourcePack.isFile() || resourcePack.isDirectory()) {
if (loadSingleResourcePack(resourcePack, loaders)) {
return true;
}
Log.infof("Missing %d resources in: %s", countMissingResources(loaders), resourcePack.getAbsolutePath());
} else {
Log.errorf("Invalid path to resource pack: %s", resourcePack.getAbsolutePath());
}
}
return false;
public static boolean loadResources(PackLoader... loaders) {
return loadResources(Arrays.asList(loaders));
}

/**
* Load resources from a single resource pack.
* Load resources from all resource packs.
*
* @return True if all resources have been found in this pack and no fallback is required.
* @return True if all resources have been found in the packs and no fallback is required.
*/
private static boolean loadSingleResourcePack(File pack, List<PackLoader> loaders) {
Log.infof("Loading %s %s", getResourcePackDescriptor(pack), pack.getAbsolutePath());
try (FileSystem resourcePack = getPackFileSystem(pack)) {
Path root = getPackRootPath(pack, resourcePack);

boolean complete = true;
for (PackLoader loader : loaders) {
if (!loader.load(root, pack.getName())) {
complete = false;
}
private static boolean loadResources(List<PackLoader> loaders) {
boolean complete = true;
for (PackLoader loader : loaders) {
if (!loader.load(resourcePacks)) {
complete = false;
}
return complete;
} catch (UnsupportedOperationException uoex) {
// default file systems do not support closing
} catch (IOException ioex) {
Log.warnf(
"Failed to open %s (%s): %s",
getResourcePackDescriptor(pack),
pack.getAbsolutePath(),
ioex.getMessage()
);
}
return false;
return complete;
}

public static String getResourcePackDescriptor(File pack) {
Expand All @@ -240,6 +214,9 @@ public static FileSystem getPackFileSystem(File pack) throws IOException {
// This catch is required for Java 8. This error appears safe to catch.
// https://stackoverflow.com/a/51715939
throw new IOException(e);
} catch (FileSystemAlreadyExistsException e) {
// for resource packs in jar or zip files (re-use existing fs)
return FileSystems.getFileSystem(URI.create("jar:" + pack.toURI()));
}
}

Expand Down
Loading

0 comments on commit ee088a7

Please sign in to comment.