feat: 资源包管理 (#4475)

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
辞庐
2025-12-06 22:17:04 +08:00
committed by GitHub
parent 04b300f453
commit ef3df92592
12 changed files with 482 additions and 49 deletions

View File

@@ -564,4 +564,8 @@ public class DefaultGameRepository implements GameRepository {
.append("baseDirectory", baseDirectory)
.toString();
}
public Path getResourcepacksDirectory(String id) {
return getRunDirectory(id).resolve("resourcepacks");
}
}

View File

@@ -219,7 +219,7 @@ public class Datapack {
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.getPackInfo().getDescription(), this));
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
} catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
} catch (IOException e) {

View File

@@ -23,9 +23,9 @@ import com.google.gson.annotations.SerializedName;
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.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -36,28 +36,12 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
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")
private final PackInfo pack;
public PackMcMeta() {
this(new PackInfo());
}
public PackMcMeta(PackInfo packInfo) {
this.pack = packInfo;
}
public PackInfo getPackInfo() {
return pack;
}
@JsonSerializable
public record PackMcMeta(@SerializedName("pack") PackInfo pack) implements Validation {
@Override
public void validate() throws JsonParseException {
if (pack == null)
@@ -65,29 +49,10 @@ public class PackMcMeta implements Validation {
}
@JsonAdapter(PackInfoDeserializer.class)
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, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList()));
}
public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) {
this.packFormat = packFormat;
this.minPackVersion = minPackVersion;
this.maxPackVersion = maxPackVersion;
this.description = description;
}
public record PackInfo(@SerializedName("pack_format") int packFormat,
@SerializedName("min_format") PackVersion minPackVersion,
@SerializedName("max_format") PackVersion maxPackVersion,
@SerializedName("description") LocalModFile.Description description) {
public PackVersion getEffectiveMinVersion() {
return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0);
}
@@ -95,10 +60,6 @@ public class PackMcMeta implements Validation {
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> {
@@ -148,7 +109,7 @@ public class PackMcMeta implements Validation {
}
}
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
public static final class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) {
List<LocalModFile.Description.Part> parts = new ArrayList<>();

View File

@@ -0,0 +1,30 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
public interface ResourcepackFile {
@Nullable
LocalModFile.Description getDescription();
String getName();
Path getPath();
byte @Nullable [] getIcon();
static ResourcepackFile parse(Path path) throws IOException {
String fileName = path.getFileName().toString();
if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
return new ResourcepackZipFile(path);
} else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) {
return new ResourcepackFolder(path);
}
return null;
}
}

View File

@@ -0,0 +1,60 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcepackFolder implements ResourcepackFile {
private final Path path;
private final LocalModFile.Description description;
private final byte @Nullable [] icon;
public ResourcepackFolder(Path path) {
this.path = path;
LocalModFile.Description description = null;
try {
description = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class).pack().description();
} catch (Exception e) {
LOG.warning("Failed to parse resourcepack meta", e);
}
byte[] icon;
try {
icon = Files.readAllBytes(path.resolve("pack.png"));
} catch (IOException e) {
icon = null;
LOG.warning("Failed to read resourcepack icon", e);
}
this.icon = icon;
this.description = description;
}
@Override
public String getName() {
return path.getFileName().toString();
}
@Override
public Path getPath() {
return path;
}
@Override
public LocalModFile.Description getDescription() {
return description;
}
@Override
public byte @Nullable [] getIcon() {
return icon;
}
}

View File

@@ -0,0 +1,72 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.tree.ZipFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcepackZipFile implements ResourcepackFile {
private final Path path;
private final byte @Nullable [] icon;
private final String name;
private final LocalModFile.Description description;
public ResourcepackZipFile(Path path) throws IOException {
this.path = path;
LocalModFile.Description description = null;
byte[] icon = null;
try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(path))) {
try {
description = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class).pack().description();
} catch (Exception e) {
LOG.warning("Failed to parse resourcepack meta", e);
}
var iconEntry = zipFileTree.getEntry("/pack.png");
if (iconEntry != null) {
try (InputStream is = zipFileTree.getInputStream(iconEntry)) {
icon = is.readAllBytes();
} catch (Exception e) {
LOG.warning("Failed to load resourcepack icon", e);
}
}
}
this.icon = icon;
this.description = description;
name = FileUtils.getNameWithoutExtension(path);
}
@Override
public String getName() {
return name;
}
@Override
public Path getPath() {
return path;
}
@Override
public LocalModFile.Description getDescription() {
return description;
}
@Override
public byte @Nullable [] getIcon() {
return icon;
}
}