feat: 增强对数据包/资源包mcmeta的解析能力 (#4612)

close #3952
This commit is contained in:
mineDiamond
2025-11-15 21:25:48 +08:00
committed by GitHub
parent 75c4fb6550
commit f13d53d6da
3 changed files with 180 additions and 46 deletions

View File

@@ -55,7 +55,6 @@ import org.jackhuang.hmcl.ui.construct.MDListCell;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.Nullable;
@@ -233,7 +232,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
String getSubtitle() {
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
return packInfo.getDescription().toString();
}
Datapack.Pack getPackInfo() {

View File

@@ -24,6 +24,8 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -37,6 +39,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@Immutable
public class PackMcMeta implements Validation {
@SerializedName("pack")
@@ -65,20 +69,31 @@ public class PackMcMeta implements Validation {
@SerializedName("pack_format")
private final int packFormat;
@SerializedName("min_format")
private final PackVersion minPackVersion;
@SerializedName("max_format")
private final PackVersion maxPackVersion;
@SerializedName("description")
private final LocalModFile.Description description;
public PackInfo() {
this(0, new LocalModFile.Description(Collections.emptyList()));
this(0, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList()));
}
public PackInfo(int packFormat, LocalModFile.Description description) {
public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) {
this.packFormat = packFormat;
this.minPackVersion = minPackVersion;
this.maxPackVersion = maxPackVersion;
this.description = description;
}
public int getPackFormat() {
return packFormat;
public PackVersion getEffectiveMinVersion() {
return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0);
}
public PackVersion getEffectiveMaxVersion() {
return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0);
}
public LocalModFile.Description getDescription() {
@@ -86,61 +101,124 @@ public class PackMcMeta implements Validation {
}
}
public record PackVersion(int majorVersion, int minorVersion) implements Comparable<PackVersion> {
public static final PackVersion UNSPECIFIED = new PackVersion(0, 0);
@Override
public String toString() {
return minorVersion != 0 ? majorVersion + "." + minorVersion : String.valueOf(majorVersion);
}
@Override
public int compareTo(PackVersion other) {
int majorCompare = Integer.compare(this.majorVersion, other.majorVersion);
if (majorCompare != 0) {
return majorCompare;
}
return Integer.compare(this.minorVersion, other.minorVersion);
}
public boolean isUnspecified() {
return this.equals(UNSPECIFIED);
}
public static PackVersion fromJson(JsonElement element) throws JsonParseException {
if (element == null || element.isJsonNull()) {
return UNSPECIFIED;
}
try {
if (element instanceof JsonPrimitive primitive && primitive.isNumber()) {
return new PackVersion(element.getAsInt(), 0);
} else if (element instanceof JsonArray jsonArray) {
if (jsonArray.size() == 1 && jsonArray.get(0) instanceof JsonPrimitive) {
return new PackVersion(jsonArray.get(0).getAsInt(), 0);
} else if (jsonArray.size() == 2 && jsonArray.get(0) instanceof JsonPrimitive && jsonArray.get(1) instanceof JsonPrimitive) {
return new PackVersion(jsonArray.get(0).getAsInt(), jsonArray.get(1).getAsInt());
} else {
LOG.warning("Datapack version array must have 1 or 2 elements, but got " + jsonArray.size());
}
}
} catch (NumberFormatException e) {
LOG.warning("Failed to parse datapack version component as a number. Value: " + element, e);
}
return UNSPECIFIED;
}
}
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
private String parseText(JsonElement json) throws JsonParseException {
if (json.isJsonPrimitive()) {
JsonPrimitive primitive = json.getAsJsonPrimitive();
if (primitive.isBoolean()) {
return Boolean.toString(primitive.getAsBoolean());
} else if (primitive.isNumber()) {
return primitive.getAsNumber().toString();
} else if (primitive.isString()) {
return primitive.getAsString();
} else {
throw new JsonParseException("pack.mcmeta text not boolean nor number nor string???");
private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) {
List<LocalModFile.Description.Part> parts = new ArrayList<>();
for (Pair<String, String> list : lists) {
parts.add(new LocalModFile.Description.Part(list.getKey(), list.getValue().isEmpty() ? color : list.getValue()));
}
} else if (json.isJsonArray()) {
JsonArray arr = json.getAsJsonArray();
if (arr.size() == 0) {
return "";
} else {
return parseText(arr.get(0));
return parts;
}
private void parseComponent(JsonElement element, List<LocalModFile.Description.Part> parts, String parentColor) throws JsonParseException {
if (parentColor == null) {
parentColor = "";
}
String color = parentColor;
if (element instanceof JsonPrimitive primitive) {
parts.addAll(pairToPart(StringUtils.parseMinecraftColorCodes(primitive.getAsString()), color));
} else if (element instanceof JsonObject jsonObj) {
if (jsonObj.get("color") instanceof JsonPrimitive primitive) {
color = primitive.getAsString();
}
if (jsonObj.get("text") instanceof JsonPrimitive primitive) {
parts.addAll(pairToPart(StringUtils.parseMinecraftColorCodes(primitive.getAsString()), color));
}
if (jsonObj.get("extra") instanceof JsonArray jsonArray) {
parseComponent(jsonArray, parts, color);
}
} else if (element instanceof JsonArray jsonArray) {
if (!jsonArray.isEmpty() && jsonArray.get(0) instanceof JsonObject jsonObj && jsonObj.get("color") instanceof JsonPrimitive primitive) {
color = primitive.getAsString();
}
for (JsonElement childElement : jsonArray) {
parseComponent(childElement, parts, color);
}
} else {
throw new JsonParseException("pack.mcmeta text should be a string, a boolean, a number or a list of raw JSON text components");
LOG.warning("Skipping unsupported element in description. Expected a string, object, or array, but got type " + element.getClass().getSimpleName() + ". Value: " + element);
}
}
public LocalModFile.Description.Part deserialize(JsonElement json, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonPrimitive()) {
return new LocalModFile.Description.Part(parseText(json));
} else if (json.isJsonObject()) {
JsonObject obj = json.getAsJsonObject();
String text = parseText(obj.get("text"));
return new LocalModFile.Description.Part(text);
} else {
throw new JsonParseException("pack.mcmeta Raw JSON text should be string or an object");
private List<LocalModFile.Description.Part> parseDescription(JsonElement json) throws JsonParseException {
List<LocalModFile.Description.Part> parts = new ArrayList<>();
if (json == null || json.isJsonNull()) {
return parts;
}
try {
parseComponent(json, parts, "");
} catch (JsonParseException | IllegalStateException e) {
parts.clear();
LOG.warning("An unexpected error occurred while parsing a description component. The description may be incomplete.", e);
}
return parts;
}
@Override
public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
List<LocalModFile.Description.Part> parts = new ArrayList<>();
JsonObject packInfo = json.getAsJsonObject();
int packFormat = packInfo.get("pack_format").getAsInt();
JsonElement description = packInfo.get("description");
if (description.isJsonPrimitive()) {
parts.add(new LocalModFile.Description.Part(parseText(description)));
} else if (description.isJsonArray()) {
for (JsonElement element : description.getAsJsonArray()) {
JsonObject descriptionPart = element.getAsJsonObject();
parts.add(new LocalModFile.Description.Part(descriptionPart.get("text").getAsString(), descriptionPart.get("color").getAsString()));
}
int packFormat;
if (packInfo.get("pack_format") instanceof JsonPrimitive primitive && primitive.isNumber()) {
packFormat = primitive.getAsInt();
} else {
throw new JsonParseException("pack.mcmeta::pack::description should be String or array of text objects with text and color fields");
packFormat = 0;
}
return new PackInfo(packFormat, new LocalModFile.Description(parts));
PackVersion minVersion = PackVersion.fromJson(packInfo.get("min_format"));
PackVersion maxVersion = PackVersion.fromJson(packInfo.get("max_format"));
List<LocalModFile.Description.Part> parts = parseDescription(packInfo.get("description"));
return new PackInfo(packFormat, minVersion, maxVersion, new LocalModFile.Description(parts));
}
}

View File

@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author huangyuhui
@@ -409,7 +411,62 @@ public final class StringUtils {
if (original.indexOf('\u00A7') < 0)
return original;
return original.replaceAll("\u00A7[0-9a-gklmnor]", "");
return original.replaceAll("\u00A7[0-9a-fk-or]", "");
}
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("\u00A7([0-9a-fk-or])");
private static final String FORMAT_CODE = "format_code";
public static List<Pair<String, String>> parseMinecraftColorCodes(String original) {
List<Pair<String, String>> pairs = new ArrayList<>();
if (isBlank(original)) {
return pairs;
}
Matcher matcher = COLOR_CODE_PATTERN.matcher(original);
String currentColor = "";
int lastIndex = 0;
while (matcher.find()) {
String text = original.substring(lastIndex, matcher.start());
if (!text.isEmpty()) {
pairs.add(new Pair<>(text, currentColor));
}
char code = matcher.group(1).charAt(0);
String newColor = switch (code) {
case '0' -> "black";
case '1' -> "dark_blue";
case '2' -> "dark_green";
case '3' -> "dark_aqua";
case '4' -> "dark_red";
case '5' -> "dark_purple";
case '6' -> "gold";
case '7' -> "gray";
case '8' -> "dark_gray";
case '9' -> "blue";
case 'a' -> "green";
case 'b' -> "aqua";
case 'c' -> "red";
case 'd' -> "light_purple";
case 'e' -> "yellow";
case 'f' -> "white";
case 'k', 'l', 'm', 'n', 'o' -> FORMAT_CODE;
case 'r' -> "";
default -> null;
};
if (newColor != null && !newColor.equals(FORMAT_CODE)) {
currentColor = newColor;
}
lastIndex = matcher.end();
}
if (lastIndex < original.length()) {
String remainingText = original.substring(lastIndex);
pairs.add(new Pair<>(remainingText, currentColor));
}
return pairs;
}
public static String parseEscapeSequence(String str) {