Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/build-publish-docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Create and publish a Docker image

on:
push:
branches: ['master']
on: [push]

env:
REGISTRY: ghcr.io
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![License](https://img.shields.io/github/license/MrKinau/ResourcePackValidator)](https://github.com/MrKinau/ResourcePackValidator/blob/master/LICENSE)
[![Discord](https://img.shields.io/discord/550764567282712583?logo=discord)](https://discord.gg/xHpCDYf)

A commandline tool to validate a Minecraft Java Edition resource pack. It runs several validations, that normally run while loading the resource pack in the vanilla client as well as some extra validations to identify issues.
A commandline tool to validate a Minecraft Java Edition resource pack. It runs several validations, that normally run when loading the resource pack in the vanilla client as well as some extra validations to identify issues.

## Commandline Arguments
- `-help` Show all available commandline arguments
Expand Down Expand Up @@ -90,6 +90,9 @@ Also check all textures if they are used by a model which already is unused?
### Animated texture frames missing / too many sprite frames
Checks if an animated texture has too many / too few frames.

### Version support
There is no way to enforce a specific minecraft version, vanilla resources from different 1.20 and 1.21 versions are merged to validate against.

## Discord
To follow the project, get support or request features or bugs you can join my Discord: https://discord.gg/xHpCDYf

Expand Down
5 changes: 5 additions & 0 deletions doc/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ This is the default config:
"logLevel": "ERROR",
"ignore": []
},
"ModelTextureReferencesResolvableValidator": {
"enabled": true,
"logLevel": "ERROR",
"ignore": []
},
"ModelOverridesExistsValidator": {
"enabled": true,
"logLevel": "ERROR",
Expand Down
3 changes: 3 additions & 0 deletions doc/VALIDATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Checks if a model has at least one texture assigned to it. This check does not f
## ModelTexturesExistsValidator
Checks if the texture files referenced in the model exists.

## ModelTextureReferencesResolvableValidator
Checks if all hashprefixed referenced textures (e.g. #side) are bound to a texture. This validation is skipped if the file is used as a parent in any other model to allow template models.

## ModelOverridesExistsValidator
Checks if item overrides are correct and the referenced model exists.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.kinau.resourcepackvalidator;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.*;
import dev.kinau.resourcepackvalidator.cache.AssetDictionary;
import dev.kinau.resourcepackvalidator.config.Config;
import dev.kinau.resourcepackvalidator.report.ReportGenerator;
Expand All @@ -13,9 +12,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.cli.*;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.*;
import java.nio.file.Files;
import java.util.logging.LogManager;

Expand Down Expand Up @@ -90,12 +87,45 @@ private void adjustLogLevel() {

private void shouldCreateAssetCache() {
if (commandLine.hasOption("createAssetCache")) {
// create new assets
JsonObject assets = new AssetDictionary().createAssets(new File(commandLine.getOptionValue("createAssetCache")));
File assetsFile = new File("vanillaassets.json");
try {
Files.writeString(assetsFile.toPath(), assets.toString());
log.info("Created assets file: {}", assetsFile.getPath());
} catch (IOException ex) {
log.error("Could not saved " + assetsFile.getPath(), ex);
log.error("Could not save {}", assetsFile.getPath(), ex);
}

// merge with old assets
JsonObject oldAssets = null;
try (InputStream stream = AssetDictionary.class.getClassLoader().getResourceAsStream("vanillaassets.json")) {
if (stream == null) throw new IllegalArgumentException("vanillaassets.json is null");
JsonElement root = JsonParser.parseReader(new InputStreamReader(stream));
if (!root.isJsonObject()) throw new IllegalArgumentException("root is not JsonObject");
oldAssets = root.getAsJsonObject();
} catch (IOException | IllegalArgumentException ex) {
log.error("Could not read vanilla assets", ex);
}
if (oldAssets == null) {
System.exit(0);
return;
}
JsonArray newFiles = assets.getAsJsonArray("files");
JsonArray mergedFiles = oldAssets.getAsJsonArray("files");
for (JsonElement newFile : newFiles) {
if (!mergedFiles.contains(newFile)) {
mergedFiles.add(newFile);
}
}
JsonObject merged = new JsonObject();
merged.add("files", mergedFiles);
File mergedAssetsFile = new File("vanillaassets_merged.json");
try {
Files.writeString(mergedAssetsFile.toPath(), merged.toString());
log.info("Created merged assets file: {}", mergedAssetsFile.getPath());
} catch (IOException ex) {
log.error("Could not save {}", mergedAssetsFile.getPath(), ex);
}
System.exit(0);
}
Expand Down
11 changes: 5 additions & 6 deletions src/main/java/dev/kinau/resourcepackvalidator/ValidationJob.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package dev.kinau.resourcepackvalidator;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.*;
import dev.kinau.resourcepackvalidator.atlas.TextureAtlas;
import dev.kinau.resourcepackvalidator.cache.AssetDictionary;
import dev.kinau.resourcepackvalidator.cache.NamespaceJsonCache;
Expand All @@ -28,6 +25,8 @@
@Getter
public class ValidationJob {

@Getter
private final Gson gson = new Gson();
private final File rootDir;
private final ValidatorRegistry registry;
private final McMetaFile mcMetaFile;
Expand All @@ -48,9 +47,9 @@ public ValidationJob(File rootDir, ValidatorRegistry registry) {
namespaces.forEach(namespace -> {
jsonCache.put(namespace, new NamespaceJsonCache(namespace));
textureCache.put(namespace, new NamespaceTextureCache(namespace));
textureAtlas.put(namespace, new TextureAtlas(namespace));
textureAtlas.put(namespace, new TextureAtlas(namespace, gson));
});
this.assetDictionary = new AssetDictionary().load();
this.assetDictionary = new AssetDictionary().load(gson);
this.fontProviderFactory = new FontProviderFactory();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public class TextureAtlas {
private final OverlayNamespace namespace;
private AtlasData data;

public TextureAtlas(OverlayNamespace namespace) {
public TextureAtlas(OverlayNamespace namespace, Gson gson) {
this.namespace = namespace;
try {
File atlasDir = FileUtils.Directory.ATLASES.getFile(namespace);
File blocksAtlas = new File(atlasDir, "blocks.json");
if (!blocksAtlas.exists()) return;
this.data = new Gson().fromJson(new FileReader(blocksAtlas), AtlasData.class);
this.data = gson.fromJson(new FileReader(blocksAtlas), AtlasData.class);
data.sources().add(new AtlasSource("directory", "item", "", null, null));
data.sources().add(new AtlasSource("directory", "block", "", null, null));
} catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package dev.kinau.resourcepackvalidator.cache;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.annotation.Nullable;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

// On Update:
Expand All @@ -31,10 +33,12 @@
@Slf4j
public class AssetDictionary {

private final Set<String> assets = new HashSet<>();
private static final Asset EMPTY_ASSET = new Asset(null, null);

private final Map<String, Asset> assets = new HashMap<>();

// TODO: Distinguish resource pack versions?
public AssetDictionary load() {
public AssetDictionary load(Gson gson) {
log.debug("Loading vanilla assets…");
assets.clear();
try (InputStream stream = AssetDictionary.class.getClassLoader().getResourceAsStream("vanillaassets.json")) {
Expand All @@ -45,7 +49,23 @@ public AssetDictionary load() {
if (!rootObject.has("files") || !rootObject.get("files").isJsonArray())
throw new IllegalArgumentException("root has no files array");
JsonArray files = rootObject.getAsJsonArray("files");
assets.addAll(files.asList().stream().map(JsonElement::getAsString).toList());
for (JsonElement assetElement : files) {
if (assetElement.isJsonPrimitive()) {
assets.put(assetElement.getAsString(), EMPTY_ASSET);
continue;
}
if (assetElement.isJsonObject()) {
JsonObject assetObject = assetElement.getAsJsonObject();
if (assetObject.isEmpty()) continue;
String key = assetObject.keySet().iterator().next();
try {
Asset asset = gson.fromJson(assetObject.getAsJsonObject(key), Asset.class);
assets.put(key, asset);
} catch (JsonSyntaxException e) {
assets.put(key, EMPTY_ASSET);
}
}
}
log.debug("Loaded {} vanilla assets", assets.size());
} catch (IOException | IllegalArgumentException ex) {
log.error("Could not read vanilla assets", ex);
Expand All @@ -62,7 +82,32 @@ public JsonObject createAssets(File file) {
fileTree.filter(path -> !path.toFile().isDirectory())
.filter(path -> !path.toFile().getName().equals(".mcassetsroot"))
.forEach(path -> {
files.add(path.toString().substring(rootPath.toString().length() + 1));
JsonObject assetObject = null;
if (path.toFile().getName().endsWith(".json")) {
try {
JsonElement jsonElement = JsonParser.parseReader(new FileReader(path.toFile()));
if (jsonElement != null && jsonElement.isJsonObject()) {
JsonObject fullObject = jsonElement.getAsJsonObject();
assetObject = new JsonObject();
if (fullObject.has("parent") && fullObject.get("parent").isJsonPrimitive()) {
assetObject.add("parent", fullObject.get("parent"));
}
if (fullObject.has("textures") && fullObject.get("textures").isJsonObject()) {
assetObject.add("textures", fullObject.get("textures"));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String relPath = path.toString().substring(rootPath.toString().length() + 1);
if (assetObject == null || assetObject.isEmpty()) {
files.add(relPath);
} else {
JsonObject dataObject = new JsonObject();
dataObject.add(relPath, assetObject);
files.add(dataObject);
}
});
} catch (IOException ex) {
log.error("Could not create assets", ex);
Expand All @@ -72,6 +117,34 @@ public JsonObject createAssets(File file) {
}

public boolean contains(String asset) {
return assets.contains(asset);
return assets.containsKey(asset);
}

public Asset getAsset(String asset) {
return assets.get(asset);
}

public Set<String> getChildren(String... asset) {
if (asset == null || asset.length == 0)
return Collections.emptySet();
return assets.entrySet().stream()
.filter(entry -> {
for (String s : asset) {
if (s.equals(entry.getValue().parent()))
return true;
}
return false;
})
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

@AllArgsConstructor
@Getter
@ToString
@Accessors(fluent = true)
public static class Asset {
@Nullable private String parent;
@Nullable private Map<String, String> textures;
}
}
22 changes: 22 additions & 0 deletions src/main/java/dev/kinau/resourcepackvalidator/utils/FileUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.kinau.resourcepackvalidator.utils;

import dev.kinau.resourcepackvalidator.cache.AssetDictionary;
import dev.kinau.resourcepackvalidator.validator.context.FileContext;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
Expand Down Expand Up @@ -167,4 +168,25 @@ public static boolean isArmorModel(File file, OverlayNamespace namespace) {

return file.toPath().startsWith(equipmentModels.toPath());
}

public static String stripNamespace(String path) {
if (path.contains(":"))
path = path.substring(path.indexOf(":") + 1);
return path;
}

public static String getRelPath(FileContext context) {
return getRelPath(context.value(), context.namespace().getNamespaceName());
}

public static String getRelPath(File file, String namespaceName) {
String relPath = "";
String[] parts = file.getPath().split("assets" + File.separator + namespaceName + File.separator + FileUtils.Directory.MODELS.getPath() + File.separator);
if (parts.length > 1)
relPath = parts[1];
if (relPath.endsWith(".json"))
relPath = relPath.substring(0, relPath.length() - 5);
relPath = stripNamespace(relPath);
return relPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ final protected <T extends JsonElement> T configValue(String key, T defaultValue
}

final protected String logPrefix() {
return String.format("[%-30s] ", getClass().getSimpleName());
return String.format("[%s] ", getClass().getSimpleName());
}

final protected ValidationResult<Output> failedError(String error, Object... args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ protected boolean skipTestCase(EmptyValidationContext context) {
.then(new ModelHasAnyOverrideValidator(configData, testSuite)
.then(new ModelOverridesExistsValidator(configData, testSuite)))
.then(new ModelRequiresOverlayOverrideValidator(configData, testSuite))
.then(new ModelParentValidator(configData, testSuite))))
.then(new ModelParentValidator(configData, testSuite))
.then(new ModelTextureReferencesResolvableValidator(configData, testSuite))))
.then(new JsonElementMapper(configData, testSuite, FileUtils.Directory.FONT)
.thenForEachElement(new FontProviderMapper(configData, testSuite)
.thenForEachElement(new FilterValidator<FontProvider, FileContext, BitmapFontProvider>(configData, testSuite, this::isBitmapFontProvider)
Expand Down
Loading