Skip to content

Commit

Permalink
Jar resource pack loader
Browse files Browse the repository at this point in the history
  • Loading branch information
Sherko231 committed Jun 16, 2024
1 parent 0119c54 commit 5092ed7
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 100 deletions.
7 changes: 6 additions & 1 deletion src/main/java/cn/nukkit/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
import cn.nukkit.potion.Effect;
import cn.nukkit.potion.Potion;
import cn.nukkit.resourcepacks.ResourcePackManager;
import cn.nukkit.resourcepacks.loader.JarPluginResourcePackLoader;
import cn.nukkit.resourcepacks.loader.ZippedResourcePackLoader;
import cn.nukkit.scheduler.ServerScheduler;
import cn.nukkit.scheduler.Task;
import cn.nukkit.utils.*;
Expand Down Expand Up @@ -491,7 +493,10 @@ public Level remove(Object key) {
convertLegacyPlayerData();

this.craftingManager = new CraftingManager();
this.resourcePackManager = new ResourcePackManager(new File(Nukkit.DATA_PATH, "resource_packs"));
this.resourcePackManager = new ResourcePackManager(
new ZippedResourcePackLoader(new File(Nukkit.DATA_PATH, "resource_packs")),
new JarPluginResourcePackLoader(new File(this.pluginPath))
);

this.pluginManager = new PluginManager(this, this.commandMap);
this.pluginManager.subscribeToPermission(Server.BROADCAST_CHANNEL_ADMINISTRATIVE, this.consoleSender);
Expand Down
39 changes: 23 additions & 16 deletions src/main/java/cn/nukkit/resourcepacks/AbstractResourcePack.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,7 @@

public abstract class AbstractResourcePack implements ResourcePack {
protected JsonObject manifest;
private UUID id = null;

protected boolean verifyManifest() {
if (this.manifest.has("format_version") && this.manifest.has("header") && this.manifest.has("modules")) {
JsonObject header = this.manifest.getAsJsonObject("header");
return header.has("description") &&
header.has("name") &&
header.has("uuid") &&
header.has("version") &&
header.getAsJsonArray("version").size() == 3;
} else {
return false;
}
}
protected UUID id = null;

@Override
public String getPackName() {
Expand All @@ -46,8 +33,28 @@ public String getPackVersion() {
version.get(2).getAsString());
}

protected boolean verifyManifest() {
if (this.manifest.has("format_version") && this.manifest.has("header") && this.manifest.has("modules")) {
JsonObject header = this.manifest.getAsJsonObject("header");
return header.has("description") &&
header.has("name") &&
header.has("uuid") &&
header.has("version") &&
header.getAsJsonArray("version").size() == 3;
} else {
return false;
}
}

@Override
public int hashCode() {
return id.hashCode();
}

@Override
public String getEncryptionKey() {
return "";
public boolean equals(Object obj) {
if (!(obj instanceof ResourcePack)) return false;
ResourcePack anotherPack = (ResourcePack) obj;
return this.id.equals(anotherPack.getPackId());
}
}
162 changes: 162 additions & 0 deletions src/main/java/cn/nukkit/resourcepacks/JarPluginResourcePack.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package cn.nukkit.resourcepacks;

import cn.nukkit.Server;
import com.google.gson.JsonParser;

import javax.annotation.Nullable;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class JarPluginResourcePack extends AbstractResourcePack {
public static final String RESOURCE_PACK_PATH = "assets/resource_pack/";
protected File jarPluginFile;
protected ByteBuffer zippedByteBuffer;
protected byte[] sha256;
protected String encryptionKey = "";

public static boolean hasResourcePack(File jarPluginFile) {
try {
return findManifestInJar(new ZipFile(jarPluginFile)) != null;
} catch (IOException e) {
return false;
}
}

@Nullable
protected static ZipEntry findManifestInJar(ZipFile jar) {
ZipEntry manifest = jar.getEntry(RESOURCE_PACK_PATH + "manifest.json");
if (manifest == null) {
manifest = jar.stream()
.filter(e -> e.getName().toLowerCase(Locale.ENGLISH).endsWith("manifest.json") && !e.isDirectory())
.filter(e -> {
File fe = new File(e.getName());
if (!fe.getName().equalsIgnoreCase("manifest.json")) {
return false;
}
return fe.getParent() == null || fe.getParentFile().getParent() == null;
})
.findFirst()
.orElse(null);
}
return manifest;
}

public JarPluginResourcePack(File jarPluginFile) {
if (!jarPluginFile.exists()) {
throw new IllegalArgumentException(Server.getInstance().getLanguage()
.translateString("nukkit.resources.zip.not-found", jarPluginFile.getName()));
}

this.jarPluginFile = jarPluginFile;


try {
ZipFile jar = new ZipFile(jarPluginFile);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
ZipEntry manifest = findManifestInJar(jar);
if (manifest == null)
throw new IllegalArgumentException(
Server.getInstance().getLanguage().translateString("nukkit.resources.zip.no-manifest"));

this.manifest = JsonParser
.parseReader(new InputStreamReader(jar.getInputStream(manifest), StandardCharsets.UTF_8))
.getAsJsonObject();

ZipEntry encryptionKeyEntry = jar.getEntry(RESOURCE_PACK_PATH + "encryption.key");
if (encryptionKeyEntry != null) {
this.encryptionKey = new String(readAllBytes(jar, encryptionKeyEntry),StandardCharsets.UTF_8);
Server.getInstance().getLogger().debug(this.encryptionKey);
}

jar.stream().forEach(entry -> {
if (entry.getName().startsWith(RESOURCE_PACK_PATH) && !entry.isDirectory() && !entry.getName().equals(RESOURCE_PACK_PATH + "encryption.key")) {
try {
zipOutputStream.putNextEntry(new ZipEntry(entry.getName().substring(RESOURCE_PACK_PATH.length())));
zipOutputStream.write(readAllBytes(jar, entry));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});

jar.close();
zipOutputStream.close();
byteArrayOutputStream.close();

zippedByteBuffer = ByteBuffer.allocateDirect(byteArrayOutputStream.size());
byte[] bytes = byteArrayOutputStream.toByteArray();
zippedByteBuffer.put(bytes);
zippedByteBuffer.flip();

try {
this.sha256 = MessageDigest.getInstance("SHA-256").digest(bytes);
} catch (Exception e) {
Server.getInstance().getLogger().error("Failed to parse the SHA-256 of the resource pack inside of jar plugin " + jarPluginFile.getName(), e);
}
} catch (IOException e) {
Server.getInstance().getLogger().error("An error occurred while loading the resource pack inside of a jar plugin " + jarPluginFile, e);
}

if (!this.verifyManifest()) {
throw new IllegalArgumentException(Server.getInstance().getLanguage()
.translateString("nukkit.resources.zip.invalid-manifest"));
}
}

@Override
public int getPackSize() {
return this.zippedByteBuffer.limit();
}

@Override
public byte[] getSha256() {
return this.sha256;
}

@Override
public String getEncryptionKey() {
return encryptionKey;
}

@Override
public byte[] getPackChunk(int off, int len) {
byte[] chunk;
if (this.getPackSize() - off > len) {
chunk = new byte[len];
} else {
chunk = new byte[this.getPackSize() - off];
}

try{
zippedByteBuffer.position(off);
zippedByteBuffer.get(chunk);
} catch (Exception e) {
Server.getInstance().getLogger().error("An error occurred while processing the resource pack " + getPackName() + " at offset:" + off + " and length: " + len, e);
}

return chunk;
}

private byte[] readAllBytes(ZipFile jar, ZipEntry encryptionKeyEntry) {
byte[] data = null;
try (InputStream is = jar.getInputStream(encryptionKeyEntry)) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] temp = new byte[1024];
while ((nRead = is.read(temp, 0, temp.length)) != -1) {
buffer.write(temp, 0, nRead);
}
data = buffer.toByteArray();
} catch (IOException e) {
System.out.println("An error occurred while reading the data: " + e.getMessage());
}
return data;
}
}
8 changes: 7 additions & 1 deletion src/main/java/cn/nukkit/resourcepacks/ResourcePack.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import java.util.UUID;

public interface ResourcePack {


ResourcePack[] EMPTY_ARRAY = new ResourcePack[0];

String getPackName();

UUID getPackId();
Expand All @@ -15,5 +19,7 @@ public interface ResourcePack {

byte[] getPackChunk(int off, int len);

String getEncryptionKey();
default String getEncryptionKey() {
return "";
}
}
91 changes: 47 additions & 44 deletions src/main/java/cn/nukkit/resourcepacks/ResourcePackManager.java
Original file line number Diff line number Diff line change
@@ -1,62 +1,65 @@
package cn.nukkit.resourcepacks;

import cn.nukkit.Server;
import com.google.common.io.Files;
import cn.nukkit.resourcepacks.loader.ResourcePackLoader;
import cn.nukkit.resourcepacks.loader.ZippedResourcePackLoader;
import com.google.common.collect.Sets;

import java.io.File;
import java.util.*;

public class ResourcePackManager {

private int maxChunkSize = 1024 * 32;// 32kb is default

private final Map<UUID, ResourcePack> resourcePacksById = new HashMap<>();
private ResourcePack[] resourcePacks;

public ResourcePackManager(File path) {
if (!path.exists()) {
path.mkdirs();
} else if (!path.isDirectory()) {
throw new IllegalArgumentException(Server.getInstance().getLanguage()
.translateString("nukkit.resources.invalid-path", path.getName()));
}

List<ResourcePack> loadedResourcePacks = new ArrayList<>();
for (File pack : path.listFiles()) {
try {
ResourcePack resourcePack = null;

String fileExt = Files.getFileExtension(pack.getName());
if (!pack.isDirectory() && !fileExt.equals("key")) { //directory resource packs temporarily unsupported
switch (fileExt) {
case "zip":
case "mcpack":
resourcePack = new ZippedResourcePack(pack);
break;
default:
Server.getInstance().getLogger().warning(Server.getInstance().getLanguage()
.translateString("nukkit.resources.unknown-format", pack.getName()));
break;
}
}

if (resourcePack != null) {
loadedResourcePacks.add(resourcePack);
this.resourcePacksById.put(resourcePack.getPackId(), resourcePack);
}
} catch (IllegalArgumentException e) {
Server.getInstance().getLogger().warning(Server.getInstance().getLanguage()
.translateString("nukkit.resources.fail", pack.getName(), e.getMessage()));
}
}

this.resourcePacks = loadedResourcePacks.toArray(new ResourcePack[0]);
Server.getInstance().getLogger().info(Server.getInstance().getLanguage()
.translateString("nukkit.resources.success", String.valueOf(this.resourcePacks.length)));
private final Set<ResourcePack> resourcePacks = new HashSet<>();
private final Set<ResourcePackLoader> loaders;


public ResourcePackManager(Set<ResourcePackLoader> loaders) {
this.loaders = loaders;
reloadPacks();
}

public ResourcePackManager(ResourcePackLoader... loaders) {
this(Sets.newHashSet(loaders));
}

public ResourcePackManager(File resourcePacksDir) {
this(new ZippedResourcePackLoader(resourcePacksDir));
}

public ResourcePack[] getResourceStack() {
return this.resourcePacks;
return this.resourcePacks.toArray(ResourcePack.EMPTY_ARRAY);
}

public ResourcePack getPackById(UUID id) {
return this.resourcePacksById.get(id);
}

public int getMaxChunkSize() {
return this.maxChunkSize;
}

public void setMaxChunkSize(int size) {
this.maxChunkSize = size;
}

public void registerPackLoader(ResourcePackLoader loader) {
this.loaders.add(loader);
}

public void reloadPacks() {
this.resourcePacksById.clear();
this.resourcePacks.clear();
this.loaders.forEach(loader -> {
List<ResourcePack> loadedPacks = loader.loadPacks();
loadedPacks.forEach(pack -> resourcePacksById.put(pack.getPackId(), pack));
this.resourcePacks.addAll(loadedPacks);
});

Server.getInstance().getLogger().info(Server.getInstance().getLanguage()
.translateString("nukkit.resources.success", String.valueOf(this.resourcePacks.size())));
}
}
Loading

0 comments on commit 5092ed7

Please sign in to comment.