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
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
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;

Expand Down Expand Up @@ -233,7 +232,7 @@ String getTitle() {
}

String getSubtitle() {
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
return packInfo.getDescription().toString();
}

Datapack.Pack getPackInfo() {
Expand Down
164 changes: 121 additions & 43 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modinfo/PackMcMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
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;
Expand All @@ -37,6 +39,8 @@
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")
Expand Down Expand Up @@ -65,82 +69,156 @@ public static class PackInfo {
@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() {
return description;
}
}

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()));
}
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();
}
} else if (json.isJsonArray()) {
JsonArray arr = json.getAsJsonArray();
if (arr.size() == 0) {
return "";
} else {
return parseText(arr.get(0));

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));
}
}

Expand Down
59 changes: 58 additions & 1 deletion HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author huangyuhui
Expand Down Expand Up @@ -409,7 +411,62 @@ public static String parseColorEscapes(String original) {
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) {
Expand Down