|
24 | 24 | import org.jackhuang.hmcl.mod.ModLoaderType; |
25 | 25 | import org.jackhuang.hmcl.mod.ModManager; |
26 | 26 | import org.jackhuang.hmcl.util.Immutable; |
| 27 | +import org.jackhuang.hmcl.util.Pair; |
| 28 | +import org.jackhuang.hmcl.util.StringUtils; |
27 | 29 | import org.jackhuang.hmcl.util.gson.JsonUtils; |
28 | 30 | import org.jackhuang.hmcl.util.gson.Validation; |
29 | 31 | import org.jackhuang.hmcl.util.io.FileUtils; |
|
37 | 39 | import java.util.Collections; |
38 | 40 | import java.util.List; |
39 | 41 |
|
| 42 | +import static org.jackhuang.hmcl.util.logging.Logger.LOG; |
| 43 | + |
40 | 44 | @Immutable |
41 | 45 | public class PackMcMeta implements Validation { |
42 | 46 | @SerializedName("pack") |
@@ -65,82 +69,156 @@ public static class PackInfo { |
65 | 69 | @SerializedName("pack_format") |
66 | 70 | private final int packFormat; |
67 | 71 |
|
| 72 | + @SerializedName("min_format") |
| 73 | + private final PackVersion minPackVersion; |
| 74 | + @SerializedName("max_format") |
| 75 | + private final PackVersion maxPackVersion; |
| 76 | + |
68 | 77 | @SerializedName("description") |
69 | 78 | private final LocalModFile.Description description; |
70 | 79 |
|
71 | 80 | public PackInfo() { |
72 | | - this(0, new LocalModFile.Description(Collections.emptyList())); |
| 81 | + this(0, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList())); |
73 | 82 | } |
74 | 83 |
|
75 | | - public PackInfo(int packFormat, LocalModFile.Description description) { |
| 84 | + public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) { |
76 | 85 | this.packFormat = packFormat; |
| 86 | + this.minPackVersion = minPackVersion; |
| 87 | + this.maxPackVersion = maxPackVersion; |
77 | 88 | this.description = description; |
78 | 89 | } |
79 | 90 |
|
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); |
82 | 97 | } |
83 | 98 |
|
84 | 99 | public LocalModFile.Description getDescription() { |
85 | 100 | return description; |
86 | 101 | } |
87 | 102 | } |
88 | 103 |
|
| 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 | + |
89 | 151 | public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> { |
90 | 152 |
|
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(); |
102 | 181 | } |
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); |
109 | 185 | } |
110 | 186 | } 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); |
112 | 188 | } |
113 | 189 | } |
114 | 190 |
|
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); |
124 | 203 | } |
| 204 | + |
| 205 | + return parts; |
125 | 206 | } |
126 | 207 |
|
127 | 208 | @Override |
128 | 209 | public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { |
129 | | - List<LocalModFile.Description.Part> parts = new ArrayList<>(); |
130 | 210 | 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(); |
140 | 214 | } 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; |
142 | 216 | } |
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)); |
144 | 222 | } |
145 | 223 | } |
146 | 224 |
|
|
0 commit comments