diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml index dd5f843e5..a2b68568f 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -353,5 +353,8 @@ warning.config.selector.invalid_target: "Issue found in file - T warning.config.resource_pack.item_model.conflict.vanilla: "Failed to generate item model for '' because this item model has been occupied by a vanilla item." warning.config.resource_pack.item_model.already_exist: "Failed to generate item model for '' because the file '' already exists." warning.config.resource_pack.model.generation.already_exist: "Failed to generate model because the model file '' already exists." -warning.config.resource_pack.generation.missing_texture: "Missing texture: ''." -warning.config.resource_pack.generation.missing_model: "Missing model: ''." \ No newline at end of file +warning.config.resource_pack.generation.missing_font_texture: "Font '' is missing required texture: ''" +warning.config.resource_pack.generation.missing_model_texture: "Model '' is missing texture ''" +warning.config.resource_pack.generation.missing_item_model: "Item '' is missing model file: ''" +warning.config.resource_pack.generation.missing_block_model: "Block '' is missing model file: ''" +warning.config.resource_pack.generation.missing_parent_model: "Model '' cannot find parent model: ''" \ No newline at end of file diff --git a/common-files/src/main/resources/translations/zh_cn.yml b/common-files/src/main/resources/translations/zh_cn.yml index bfe202fce..4f11d3878 100644 --- a/common-files/src/main/resources/translations/zh_cn.yml +++ b/common-files/src/main/resources/translations/zh_cn.yml @@ -352,4 +352,9 @@ warning.config.selector.invalid_type: "在文件 中发现问题 warning.config.selector.invalid_target: "在文件 中发现问题 - 配置项 '' 使用了无效的选择器目标 ''" warning.config.resource_pack.item_model.conflict.vanilla: "无法为 '' 生成物品模型,因为该物品模型已被原版物品占用" warning.config.resource_pack.item_model.already_exist: "无法为 '' 生成物品模型,因为文件 '' 已存在" -warning.config.resource_pack.model.generation.already_exist: "无法生成模型,因为模型文件 '' 已存在" \ No newline at end of file +warning.config.resource_pack.model.generation.already_exist: "无法生成模型,因为模型文件 '' 已存在" +warning.config.resource_pack.generation.missing_font_texture: "字体''缺少必要纹理: ''" +warning.config.resource_pack.generation.missing_model_texture: "模型''缺少纹理''" +warning.config.resource_pack.generation.missing_item_model: "物品''缺少模型文件: ''" +warning.config.resource_pack.generation.missing_block_model: "方块''缺少模型文件: ''" +warning.config.resource_pack.generation.missing_parent_model: "模型''找不到父级模型文件: ''" \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java index 62fd9778c..aecf1a92b 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java @@ -1,5 +1,6 @@ package net.momirealms.craftengine.core.pack; +import com.google.common.collect.*; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import com.google.gson.*; @@ -20,6 +21,7 @@ import net.momirealms.craftengine.core.pack.model.LegacyOverridesModel; import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration; import net.momirealms.craftengine.core.pack.model.generation.ModelGenerator; import net.momirealms.craftengine.core.pack.obfuscation.ObfA; +import net.momirealms.craftengine.core.pack.obfuscation.ObfH; import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.config.Config; import net.momirealms.craftengine.core.plugin.config.ConfigParser; @@ -111,7 +113,7 @@ public abstract class AbstractPackManager implements PackManager { VANILLA_TEXTURES.add(Key.of("minecraft", "trims/items/" + trimItem + "_" + trimColorPalette)); } } - + loadInternalList("models", "block/", VANILLA_MODELS::add); loadInternalList("models", "item/", VANILLA_MODELS::add); } @@ -594,6 +596,9 @@ public abstract class AbstractPackManager implements PackManager { this.generateClientLang(generatedPackPath); this.generateEquipments(generatedPackPath); this.generateParticle(generatedPackPath); + if (Config.validateResourcePack()) { + this.validateResourcePack(generatedPackPath); + } Path finalPath = resourcePackPath(); Files.createDirectories(finalPath.getParent()); try { @@ -607,6 +612,234 @@ public abstract class AbstractPackManager implements PackManager { } } + @SuppressWarnings("DuplicatedCode") + private void validateResourcePack(Path path) throws IOException { + List rootPaths = FileUtils.collectOverlays(path); + + Multimap imageToFonts = ArrayListMultimap.create(); // 图片到字体的映射 + Multimap modelToItems = ArrayListMultimap.create(); // 模型到物品的映射 + Multimap modelToBlocks = ArrayListMultimap.create(); // 模型到方块的映射 + Multimap imageToModels = ArrayListMultimap.create(); // 图片到模型的映射 + + for (Path rootPath : rootPaths) { + Path assetsPath = rootPath.resolve("assets"); + if (!Files.isDirectory(assetsPath)) continue; + + for (Path namespacePath : FileUtils.collectNamespaces(assetsPath)) { + Path fontPath = namespacePath.resolve("font"); + if (Files.isDirectory(fontPath)) { + Files.walkFileTree(fontPath, new SimpleFileVisitor<>() { + @Override + public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException { + if (!isJsonFile(file)) return FileVisitResult.CONTINUE; + JsonObject fontJson = GsonHelper.readJsonFile(file).getAsJsonObject(); + JsonArray providers = fontJson.getAsJsonArray("providers"); + if (providers != null) { + Key fontName = Key.of(namespacePath.getFileName().toString(), FileUtils.pathWithoutExtension(file.getFileName().toString())); + for (JsonElement provider : providers) { + if (provider instanceof JsonObject providerJO && providerJO.has("type")) { + String type = providerJO.get("type").getAsString(); + if (type.equals("bitmap") && providerJO.has("file")) { + String pngFile = providerJO.get("file").getAsString(); + Key resourceLocation = Key.of(FileUtils.pathWithoutExtension(pngFile)); + imageToFonts.put(resourceLocation, fontName); + } + } + } + } + return FileVisitResult.CONTINUE; + } + }); + } + + Path itemsPath = namespacePath.resolve("items"); + if (Files.isDirectory(itemsPath)) { + Files.walkFileTree(itemsPath, new SimpleFileVisitor<>() { + @Override + public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException { + if (!isJsonFile(file)) return FileVisitResult.CONTINUE; + JsonObject itemJson = GsonHelper.readJsonFile(file).getAsJsonObject(); + Key item = Key.of(namespacePath.getFileName().toString(), FileUtils.pathWithoutExtension(file.getFileName().toString())); + collectItemModelsDeeply(itemJson, (resourceLocation) -> modelToItems.put(resourceLocation, item)); + return FileVisitResult.CONTINUE; + } + }); + } + + Path blockStatesPath = namespacePath.resolve("blockstates"); + if (Files.isDirectory(blockStatesPath)) { + Files.walkFileTree(blockStatesPath, new SimpleFileVisitor<>() { + @Override + public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException { + if (!isJsonFile(file)) return FileVisitResult.CONTINUE; + String blockId = FileUtils.pathWithoutExtension(file.getFileName().toString()); + JsonObject blockStateJson = GsonHelper.readJsonFile(file).getAsJsonObject(); + if (blockStateJson.has("multipart")) { + collectMultipart(blockStateJson.getAsJsonArray("multipart"), (location) -> modelToBlocks.put(location, blockId)); + } else if (blockStateJson.has("variants")) { + collectVariants(blockId, blockStateJson.getAsJsonObject("variants"), modelToBlocks::put); + } + return FileVisitResult.CONTINUE; + } + }); + } + } + } + + label: for (Map.Entry> entry : imageToFonts.asMap().entrySet()) { + Key key = entry.getKey(); + if (VANILLA_TEXTURES.contains(key)) continue; + String imagePath = "assets/" + key.namespace() + "/textures/" + key.value() + ".png"; + for (Path rootPath : rootPaths) { + if (Files.exists(rootPath.resolve(imagePath))) { + continue label; + } + } + TranslationManager.instance().log("warning.config.resource_pack.generation.missing_font_texture", entry.getValue().stream().distinct().toList().toString(), imagePath); + } + + label: for (Map.Entry> entry : modelToItems.asMap().entrySet()) { + Key key = entry.getKey(); + String modelPath = "assets/" + key.namespace() + "/models/" + key.value() + ".json"; + if (VANILLA_MODELS.contains(key)) continue; + for (Path rootPath : rootPaths) { + Path modelJsonPath = rootPath.resolve(modelPath); + if (Files.exists(rootPath.resolve(modelPath))) { + collectModels(key, GsonHelper.readJsonFile(modelJsonPath).getAsJsonObject(), rootPaths, imageToModels); + continue label; + } + } + TranslationManager.instance().log("warning.config.resource_pack.generation.missing_item_model", entry.getValue().stream().distinct().toList().toString(), modelPath); + } + + label: for (Map.Entry> entry : modelToBlocks.asMap().entrySet()) { + Key key = entry.getKey(); + String modelPath = "assets/" + key.namespace() + "/models/" + key.value() + ".json"; + if (VANILLA_MODELS.contains(key)) continue; + for (Path rootPath : rootPaths) { + Path modelJsonPath = rootPath.resolve(modelPath); + if (Files.exists(modelJsonPath)) { + collectModels(key, GsonHelper.readJsonFile(modelJsonPath).getAsJsonObject(), rootPaths, imageToModels); + continue label; + } + } + TranslationManager.instance().log("warning.config.resource_pack.generation.missing_block_model", entry.getValue().stream().distinct().toList().toString(), modelPath); + } + + label: for (Map.Entry> entry : imageToModels.asMap().entrySet()) { + Key key = entry.getKey(); + if (VANILLA_TEXTURES.contains(key)) continue; + String imagePath = "assets/" + key.namespace() + "/textures/" + key.value() + ".png"; + for (Path rootPath : rootPaths) { + if (Files.exists(rootPath.resolve(imagePath))) { + continue label; + } + } + TranslationManager.instance().log("warning.config.resource_pack.generation.missing_model_texture", entry.getValue().stream().distinct().toList().toString(), imagePath); + } + } + + private static void collectModels(Key model, JsonObject modelJson, List rootPaths, Multimap imageToModels) throws IOException { + if (modelJson.has("parent")) { + Key parentResourceLocation = Key.from(modelJson.get("parent").getAsString()); + if (!VANILLA_MODELS.contains(parentResourceLocation)) { + String parentModelPath = "assets/" + parentResourceLocation.namespace() + "/models/" + parentResourceLocation.value() + ".json"; + label: { + for (Path rootPath : rootPaths) { + Path modelJsonPath = rootPath.resolve(parentModelPath); + if (Files.exists(modelJsonPath)) { + collectModels(parentResourceLocation, GsonHelper.readJsonFile(modelJsonPath).getAsJsonObject(), rootPaths, imageToModels); + break label; + } + } + TranslationManager.instance().log("warning.config.resource_pack.generation.missing_parent_model", model.asString(), parentModelPath); + } + } + } + if (modelJson.has("textures")) { + JsonObject textures = modelJson.get("textures").getAsJsonObject(); + for (Map.Entry entry : textures.entrySet()) { +// String textureId = entry.getKey(); + String value = entry.getValue().getAsString(); + Key textureResourceLocation = Key.from(value); + imageToModels.put(textureResourceLocation, model); + } + } + } + + private static void collectMultipart(JsonArray jsonArray, Consumer callback) { + for (JsonElement element : jsonArray) { + if (element instanceof JsonObject jo) { + JsonElement applyJE = jo.get("apply"); + if (applyJE instanceof JsonObject applyJO) { + String modelPath = applyJO.get("model").getAsString(); + Key location = Key.from(modelPath); + callback.accept(location); + } else if (applyJE instanceof JsonArray applyJA) { + for (JsonElement applyInnerJE : applyJA) { + if (applyInnerJE instanceof JsonObject applyInnerJO) { + String modelPath = applyInnerJO.get("model").getAsString(); + Key location = Key.from(modelPath); + callback.accept(location); + } + } + } + } + } + } + + private static void collectVariants(String block, JsonObject jsonObject, BiConsumer callback) { + for (Map.Entry entry : jsonObject.entrySet()) { + if (entry.getValue() instanceof JsonObject entryJO) { + String modelPath = entryJO.get("model").getAsString(); + Key location = Key.from(modelPath); + callback.accept(location, block + "[" + entry.getKey() + "]"); + } else if (entry.getValue() instanceof JsonArray entryJA) { + for (JsonElement entryInnerJE : entryJA) { + if (entryInnerJE instanceof JsonObject entryJO) { + String modelPath = entryJO.get("model").getAsString(); + Key location = Key.from(modelPath); + callback.accept(location, block + "[" + entry.getKey() + "]"); + } + } + } + } + } + + private static boolean isJsonFile(Path filePath) { + String fileName = filePath.getFileName().toString(); + return fileName.endsWith(".json") || fileName.endsWith(".mcmeta"); + } + + private static void collectItemModelsDeeply(JsonObject jo, Consumer callback) { + JsonElement modelJE = jo.get("model"); + if (modelJE instanceof JsonPrimitive jsonPrimitive) { + Key location = Key.from(jsonPrimitive.getAsString()); + callback.accept(location); + return; + } + if (jo.has("type") && jo.has("base")) { + if (jo.get("type") instanceof JsonPrimitive jp1 && jo.get("base") instanceof JsonPrimitive jp2) { + String type = jp1.getAsString(); + if (type.equals("minecraft:special") || type.equals("special")) { + Key location = Key.from(jp2.getAsString()); + callback.accept(location); + } + } + } + for (Map.Entry entry : jo.entrySet()) { + if (entry.getValue() instanceof JsonObject innerJO) { + collectItemModelsDeeply(innerJO, callback); + } else if (entry.getValue() instanceof JsonArray innerJA) { + for (JsonElement innerElement : innerJA) { + if (innerElement instanceof JsonObject innerJO) { + collectItemModelsDeeply(innerJO, callback); + } + } + } + } + } + private void generateParticle(Path generatedPackPath) { if (!Config.removeTintedLeavesParticle()) return; if (Config.packMaxVersion() < 21.49f) return; diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/FileUtils.java b/core/src/main/java/net/momirealms/craftengine/core/util/FileUtils.java index 1d2a9a5b6..f82b81e7f 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/FileUtils.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/FileUtils.java @@ -1,5 +1,8 @@ package net.momirealms.craftengine.core.util; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.momirealms.craftengine.core.pack.ResourceLocation; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -57,4 +60,28 @@ public class FileUtils { throw new RuntimeException("Failed to traverse directory: " + configFolder, e); } } + + public static List collectOverlays(Path resourcePackFolder) throws IOException { + List folders = new ObjectArrayList<>(); + folders.add(resourcePackFolder); + try (Stream paths = Files.list(resourcePackFolder)) { + folders.addAll(paths + .filter(Files::isDirectory) + .filter(path -> !path.getFileName().toString().equals("assets")) + .filter(path -> Files.exists(path.resolve("assets"))) + .toList()); + } + return folders; + } + + public static List collectNamespaces(Path assetsFolder) throws IOException { + List folders; + try (Stream paths = Files.list(assetsFolder)) { + folders = new ObjectArrayList<>(paths + .filter(Files::isDirectory) + .filter(path -> ResourceLocation.isValidNamespace(path.getFileName().toString())) + .toList()); + } + return folders; + } } diff --git a/gradle.properties b/gradle.properties index 3bb896090..6e05db11f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -41,7 +41,7 @@ commons_io_version=2.18.0 commons_imaging_version=1.0.0-alpha6 commons_lang3_version=3.17.0 sparrow_nbt_version=0.9.1 -sparrow_util_version=0.49 +sparrow_util_version=0.49.1 fastutil_version=8.5.15 netty_version=4.1.121.Final joml_version=1.10.8