Skip to content

Commit f13d53d

Browse files
authored
feat: 增强对数据包/资源包mcmeta的解析能力 (#4612)
close #3952
1 parent 75c4fb6 commit f13d53d

3 files changed

Lines changed: 180 additions & 46 deletions

File tree

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
5656
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
5757
import org.jackhuang.hmcl.util.Holder;
58-
import org.jackhuang.hmcl.util.StringUtils;
5958
import org.jackhuang.hmcl.util.io.CompressingUtils;
6059
import org.jetbrains.annotations.Nullable;
6160

@@ -233,7 +232,7 @@ String getTitle() {
233232
}
234233

235234
String getSubtitle() {
236-
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
235+
return packInfo.getDescription().toString();
237236
}
238237

239238
Datapack.Pack getPackInfo() {

HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java

Lines changed: 121 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.jackhuang.hmcl.mod.ModLoaderType;
2525
import org.jackhuang.hmcl.mod.ModManager;
2626
import org.jackhuang.hmcl.util.Immutable;
27+
import org.jackhuang.hmcl.util.Pair;
28+
import org.jackhuang.hmcl.util.StringUtils;
2729
import org.jackhuang.hmcl.util.gson.JsonUtils;
2830
import org.jackhuang.hmcl.util.gson.Validation;
2931
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -37,6 +39,8 @@
3739
import java.util.Collections;
3840
import java.util.List;
3941

42+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
43+
4044
@Immutable
4145
public class PackMcMeta implements Validation {
4246
@SerializedName("pack")
@@ -65,82 +69,156 @@ public static class PackInfo {
6569
@SerializedName("pack_format")
6670
private final int packFormat;
6771

72+
@SerializedName("min_format")
73+
private final PackVersion minPackVersion;
74+
@SerializedName("max_format")
75+
private final PackVersion maxPackVersion;
76+
6877
@SerializedName("description")
6978
private final LocalModFile.Description description;
7079

7180
public PackInfo() {
72-
this(0, new LocalModFile.Description(Collections.emptyList()));
81+
this(0, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList()));
7382
}
7483

75-
public PackInfo(int packFormat, LocalModFile.Description description) {
84+
public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) {
7685
this.packFormat = packFormat;
86+
this.minPackVersion = minPackVersion;
87+
this.maxPackVersion = maxPackVersion;
7788
this.description = description;
7889
}
7990

80-
public int getPackFormat() {
81-
return packFormat;
91+
public PackVersion getEffectiveMinVersion() {
92+
return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0);
93+
}
94+
95+
public PackVersion getEffectiveMaxVersion() {
96+
return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0);
8297
}
8398

8499
public LocalModFile.Description getDescription() {
85100
return description;
86101
}
87102
}
88103

104+
public record PackVersion(int majorVersion, int minorVersion) implements Comparable<PackVersion> {
105+
106+
public static final PackVersion UNSPECIFIED = new PackVersion(0, 0);
107+
108+
@Override
109+
public String toString() {
110+
return minorVersion != 0 ? majorVersion + "." + minorVersion : String.valueOf(majorVersion);
111+
}
112+
113+
@Override
114+
public int compareTo(PackVersion other) {
115+
int majorCompare = Integer.compare(this.majorVersion, other.majorVersion);
116+
if (majorCompare != 0) {
117+
return majorCompare;
118+
}
119+
return Integer.compare(this.minorVersion, other.minorVersion);
120+
}
121+
122+
public boolean isUnspecified() {
123+
return this.equals(UNSPECIFIED);
124+
}
125+
126+
public static PackVersion fromJson(JsonElement element) throws JsonParseException {
127+
if (element == null || element.isJsonNull()) {
128+
return UNSPECIFIED;
129+
}
130+
131+
try {
132+
if (element instanceof JsonPrimitive primitive && primitive.isNumber()) {
133+
return new PackVersion(element.getAsInt(), 0);
134+
} else if (element instanceof JsonArray jsonArray) {
135+
if (jsonArray.size() == 1 && jsonArray.get(0) instanceof JsonPrimitive) {
136+
return new PackVersion(jsonArray.get(0).getAsInt(), 0);
137+
} else if (jsonArray.size() == 2 && jsonArray.get(0) instanceof JsonPrimitive && jsonArray.get(1) instanceof JsonPrimitive) {
138+
return new PackVersion(jsonArray.get(0).getAsInt(), jsonArray.get(1).getAsInt());
139+
} else {
140+
LOG.warning("Datapack version array must have 1 or 2 elements, but got " + jsonArray.size());
141+
}
142+
}
143+
} catch (NumberFormatException e) {
144+
LOG.warning("Failed to parse datapack version component as a number. Value: " + element, e);
145+
}
146+
147+
return UNSPECIFIED;
148+
}
149+
}
150+
89151
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
90152

91-
private String parseText(JsonElement json) throws JsonParseException {
92-
if (json.isJsonPrimitive()) {
93-
JsonPrimitive primitive = json.getAsJsonPrimitive();
94-
if (primitive.isBoolean()) {
95-
return Boolean.toString(primitive.getAsBoolean());
96-
} else if (primitive.isNumber()) {
97-
return primitive.getAsNumber().toString();
98-
} else if (primitive.isString()) {
99-
return primitive.getAsString();
100-
} else {
101-
throw new JsonParseException("pack.mcmeta text not boolean nor number nor string???");
153+
private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) {
154+
List<LocalModFile.Description.Part> parts = new ArrayList<>();
155+
for (Pair<String, String> list : lists) {
156+
parts.add(new LocalModFile.Description.Part(list.getKey(), list.getValue().isEmpty() ? color : list.getValue()));
157+
}
158+
return parts;
159+
}
160+
161+
private void parseComponent(JsonElement element, List<LocalModFile.Description.Part> parts, String parentColor) throws JsonParseException {
162+
if (parentColor == null) {
163+
parentColor = "";
164+
}
165+
String color = parentColor;
166+
if (element instanceof JsonPrimitive primitive) {
167+
parts.addAll(pairToPart(StringUtils.parseMinecraftColorCodes(primitive.getAsString()), color));
168+
} else if (element instanceof JsonObject jsonObj) {
169+
if (jsonObj.get("color") instanceof JsonPrimitive primitive) {
170+
color = primitive.getAsString();
171+
}
172+
if (jsonObj.get("text") instanceof JsonPrimitive primitive) {
173+
parts.addAll(pairToPart(StringUtils.parseMinecraftColorCodes(primitive.getAsString()), color));
174+
}
175+
if (jsonObj.get("extra") instanceof JsonArray jsonArray) {
176+
parseComponent(jsonArray, parts, color);
177+
}
178+
} else if (element instanceof JsonArray jsonArray) {
179+
if (!jsonArray.isEmpty() && jsonArray.get(0) instanceof JsonObject jsonObj && jsonObj.get("color") instanceof JsonPrimitive primitive) {
180+
color = primitive.getAsString();
102181
}
103-
} else if (json.isJsonArray()) {
104-
JsonArray arr = json.getAsJsonArray();
105-
if (arr.size() == 0) {
106-
return "";
107-
} else {
108-
return parseText(arr.get(0));
182+
183+
for (JsonElement childElement : jsonArray) {
184+
parseComponent(childElement, parts, color);
109185
}
110186
} else {
111-
throw new JsonParseException("pack.mcmeta text should be a string, a boolean, a number or a list of raw JSON text components");
187+
LOG.warning("Skipping unsupported element in description. Expected a string, object, or array, but got type " + element.getClass().getSimpleName() + ". Value: " + element);
112188
}
113189
}
114190

115-
public LocalModFile.Description.Part deserialize(JsonElement json, JsonDeserializationContext context) throws JsonParseException {
116-
if (json.isJsonPrimitive()) {
117-
return new LocalModFile.Description.Part(parseText(json));
118-
} else if (json.isJsonObject()) {
119-
JsonObject obj = json.getAsJsonObject();
120-
String text = parseText(obj.get("text"));
121-
return new LocalModFile.Description.Part(text);
122-
} else {
123-
throw new JsonParseException("pack.mcmeta Raw JSON text should be string or an object");
191+
private List<LocalModFile.Description.Part> parseDescription(JsonElement json) throws JsonParseException {
192+
List<LocalModFile.Description.Part> parts = new ArrayList<>();
193+
194+
if (json == null || json.isJsonNull()) {
195+
return parts;
196+
}
197+
198+
try {
199+
parseComponent(json, parts, "");
200+
} catch (JsonParseException | IllegalStateException e) {
201+
parts.clear();
202+
LOG.warning("An unexpected error occurred while parsing a description component. The description may be incomplete.", e);
124203
}
204+
205+
return parts;
125206
}
126207

127208
@Override
128209
public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
129-
List<LocalModFile.Description.Part> parts = new ArrayList<>();
130210
JsonObject packInfo = json.getAsJsonObject();
131-
int packFormat = packInfo.get("pack_format").getAsInt();
132-
JsonElement description = packInfo.get("description");
133-
if (description.isJsonPrimitive()) {
134-
parts.add(new LocalModFile.Description.Part(parseText(description)));
135-
} else if (description.isJsonArray()) {
136-
for (JsonElement element : description.getAsJsonArray()) {
137-
JsonObject descriptionPart = element.getAsJsonObject();
138-
parts.add(new LocalModFile.Description.Part(descriptionPart.get("text").getAsString(), descriptionPart.get("color").getAsString()));
139-
}
211+
int packFormat;
212+
if (packInfo.get("pack_format") instanceof JsonPrimitive primitive && primitive.isNumber()) {
213+
packFormat = primitive.getAsInt();
140214
} else {
141-
throw new JsonParseException("pack.mcmeta::pack::description should be String or array of text objects with text and color fields");
215+
packFormat = 0;
142216
}
143-
return new PackInfo(packFormat, new LocalModFile.Description(parts));
217+
PackVersion minVersion = PackVersion.fromJson(packInfo.get("min_format"));
218+
PackVersion maxVersion = PackVersion.fromJson(packInfo.get("max_format"));
219+
220+
List<LocalModFile.Description.Part> parts = parseDescription(packInfo.get("description"));
221+
return new PackInfo(packFormat, minVersion, maxVersion, new LocalModFile.Description(parts));
144222
}
145223
}
146224

HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.io.PrintWriter;
2121
import java.io.StringWriter;
2222
import java.util.*;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2325

2426
/**
2527
* @author huangyuhui
@@ -409,7 +411,62 @@ public static String parseColorEscapes(String original) {
409411
if (original.indexOf('\u00A7') < 0)
410412
return original;
411413

412-
return original.replaceAll("\u00A7[0-9a-gklmnor]", "");
414+
return original.replaceAll("\u00A7[0-9a-fk-or]", "");
415+
}
416+
417+
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("\u00A7([0-9a-fk-or])");
418+
private static final String FORMAT_CODE = "format_code";
419+
420+
public static List<Pair<String, String>> parseMinecraftColorCodes(String original) {
421+
List<Pair<String, String>> pairs = new ArrayList<>();
422+
if (isBlank(original)) {
423+
return pairs;
424+
}
425+
Matcher matcher = COLOR_CODE_PATTERN.matcher(original);
426+
String currentColor = "";
427+
int lastIndex = 0;
428+
429+
while (matcher.find()) {
430+
String text = original.substring(lastIndex, matcher.start());
431+
if (!text.isEmpty()) {
432+
pairs.add(new Pair<>(text, currentColor));
433+
}
434+
435+
char code = matcher.group(1).charAt(0);
436+
String newColor = switch (code) {
437+
case '0' -> "black";
438+
case '1' -> "dark_blue";
439+
case '2' -> "dark_green";
440+
case '3' -> "dark_aqua";
441+
case '4' -> "dark_red";
442+
case '5' -> "dark_purple";
443+
case '6' -> "gold";
444+
case '7' -> "gray";
445+
case '8' -> "dark_gray";
446+
case '9' -> "blue";
447+
case 'a' -> "green";
448+
case 'b' -> "aqua";
449+
case 'c' -> "red";
450+
case 'd' -> "light_purple";
451+
case 'e' -> "yellow";
452+
case 'f' -> "white";
453+
case 'k', 'l', 'm', 'n', 'o' -> FORMAT_CODE;
454+
case 'r' -> "";
455+
default -> null;
456+
};
457+
458+
if (newColor != null && !newColor.equals(FORMAT_CODE)) {
459+
currentColor = newColor;
460+
}
461+
462+
lastIndex = matcher.end();
463+
}
464+
465+
if (lastIndex < original.length()) {
466+
String remainingText = original.substring(lastIndex);
467+
pairs.add(new Pair<>(remainingText, currentColor));
468+
}
469+
return pairs;
413470
}
414471

415472
public static String parseEscapeSequence(String str) {

0 commit comments

Comments
 (0)