Skip to content

Commit

Permalink
Add support for manually supplying Bedrock resource packs (#1076)
Browse files Browse the repository at this point in the history
* send resource packs

A lot of this code is nukkit-credits in the classes

* send resource packs

A lot of this code is nukkit-credits in the classes

* Remove unnecessary code/debugs

* use separately generated hashes

* Updated mappings and added .mcpack support

* "packs" directory auto-create (#484)

* "packs" directory auto-create

* cleaned indentation in ResourcePack.java

* Cleaned ResourcePack.java

* Another cleanup

I hate editor on github.

* Yet another

* Another indentation cleanup

* Fix resource pack loading

(cherry picked from commit f93b074)

* Move back to internal sha256 hashing

(cherry picked from commit 812a3d8)

* Add resource pack loading back after merge

* Add comments, config option and removed unused files

* Fix packs folder location and cleanup code

* Move to better options for the client

* Fix typos in comments

* Fix pack loading

* Try to make it compile

* Final touches?

* Add Javadoc for MathUtils#constrain

Co-authored-by: EOT3000 <[email protected]>
Co-authored-by: Vesek <[email protected]>
Co-authored-by: Heath123 <[email protected]>
Co-authored-by: Camotoy <[email protected]>
  • Loading branch information
5 people authored Sep 17, 2020
1 parent 1a49e88 commit 99e72f3
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -241,4 +241,5 @@ config.yml
logs/
public-key.pem
locales/
/cache/
/cache/
/packs/
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

package org.geysermc.connector;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nukkitx.network.raknet.RakNetConstants;
Expand Down Expand Up @@ -56,6 +57,7 @@
import org.geysermc.connector.utils.DimensionUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.LocaleUtils;
import org.geysermc.connector.utils.ResourcePack;

import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext;
Expand All @@ -73,7 +75,7 @@
@Getter
public class GeyserConnector {

public static final ObjectMapper JSON_MAPPER = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES);
public static final ObjectMapper JSON_MAPPER = new ObjectMapper().enable(JsonParser.Feature.IGNORE_UNDEFINED).enable(JsonParser.Feature.ALLOW_COMMENTS).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

public static final String NAME = "Geyser";
public static final String VERSION = "DEV"; // A fallback for running in IDEs
Expand Down Expand Up @@ -136,6 +138,8 @@ private GeyserConnector(PlatformType platformType, GeyserBootstrap bootstrap) {
SoundRegistry.init();
SoundHandlerRegistry.init();

ResourcePack.loadPacks();

if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) {
// Set the remote address to localhost since that is where we are always connecting
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public interface GeyserConfiguration {

boolean isCacheChunks();

boolean isForceResourcePacks();

int getCacheImages();

IMetricsInfo getMetrics();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("above-bedrock-nether-building")
private boolean aboveBedrockNetherBuilding = false;

@JsonProperty("force-resource-packs")
private boolean forceResourcePacks = true;

private MetricsInfo metrics;

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,24 @@
package org.geysermc.connector.network;

import com.nukkitx.protocol.bedrock.BedrockPacket;
import com.nukkitx.protocol.bedrock.data.ResourcePackType;
import com.nukkitx.protocol.bedrock.BedrockPacketCodec;
import com.nukkitx.protocol.bedrock.packet.*;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.common.AuthType;
import org.geysermc.connector.configuration.GeyserConfiguration;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
import org.geysermc.connector.utils.LoginEncryptionUtils;
import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.utils.LoginEncryptionUtils;
import org.geysermc.connector.utils.MathUtils;
import org.geysermc.connector.utils.ResourcePack;
import org.geysermc.connector.utils.ResourcePackManifest;
import org.geysermc.connector.utils.SettingsUtils;

import java.io.FileInputStream;
import java.io.InputStream;

public class UpstreamPacketHandler extends LoggingPacketHandler {

public UpstreamPacketHandler(GeyserConnector connector, GeyserSession session) {
Expand Down Expand Up @@ -70,6 +77,11 @@ public boolean handle(LoginPacket loginPacket) {
session.sendUpstreamPacket(playStatus);

ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
for(ResourcePack resourcePack : ResourcePack.PACKS.values()) {
ResourcePackManifest.Header header = resourcePack.getManifest().getHeader();
resourcePacksInfo.getResourcePackInfos().add(new ResourcePacksInfoPacket.Entry(header.getUuid().toString(), header.getVersionString(), resourcePack.getFile().length(), "", "", "", false));
}
resourcePacksInfo.setForcedToAccept(GeyserConnector.getInstance().getConfig().isForceResourcePacks());
session.sendUpstreamPacket(resourcePacksInfo);
return true;
}
Expand All @@ -81,13 +93,42 @@ public boolean handle(ResourcePackClientResponsePacket packet) {
session.connect(connector.getRemoteServer());
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.connect", session.getAuthData().getName()));
break;

case SEND_PACKS:
for(String id : packet.getPackIds()) {
ResourcePackDataInfoPacket data = new ResourcePackDataInfoPacket();
String[] packID = id.split("_");
ResourcePack pack = ResourcePack.PACKS.get(packID[0]);
ResourcePackManifest.Header header = pack.getManifest().getHeader();

data.setPackId(header.getUuid());
int chunkCount = (int) Math.ceil((int) pack.getFile().length() / (double) ResourcePack.CHUNK_SIZE);
data.setChunkCount(chunkCount);
data.setCompressedPackSize(pack.getFile().length());
data.setMaxChunkSize(ResourcePack.CHUNK_SIZE);
data.setHash(pack.getSha256());
data.setPackVersion(packID[1]);
data.setPremium(false);
data.setType(ResourcePackType.RESOURCE);

session.sendUpstreamPacket(data);
}
break;

case HAVE_ALL_PACKS:
ResourcePackStackPacket stack = new ResourcePackStackPacket();
stack.setExperimental(false);
stack.setForcedToAccept(false);
stack.setGameVersion("*");
session.sendUpstreamPacket(stack);
ResourcePackStackPacket stackPacket = new ResourcePackStackPacket();
stackPacket.setExperimental(false);
stackPacket.setForcedToAccept(false); // Leaving this as false allows the player to choose to download or not
stackPacket.setGameVersion(session.getClientData().getGameVersion());

for (ResourcePack pack : ResourcePack.PACKS.values()) {
ResourcePackManifest.Header header = pack.getManifest().getHeader();
stackPacket.getResourcePacks().add(new ResourcePackStackPacket.Entry(header.getUuid().toString(), header.getVersionString(), ""));
}

session.sendUpstreamPacket(stackPacket);
break;

default:
session.disconnect("disconnectionScreen.resourcePack");
break;
Expand Down Expand Up @@ -149,4 +190,30 @@ public boolean handle(MovePlayerPacket packet) {
boolean defaultHandler(BedrockPacket packet) {
return translateAndDefault(packet);
}

@Override
public boolean handle(ResourcePackChunkRequestPacket packet) {
ResourcePackChunkDataPacket data = new ResourcePackChunkDataPacket();
ResourcePack pack = ResourcePack.PACKS.get(packet.getPackId().toString());

data.setChunkIndex(packet.getChunkIndex());
data.setProgress(packet.getChunkIndex() * ResourcePack.CHUNK_SIZE);
data.setPackVersion(packet.getPackVersion());
data.setPackId(packet.getPackId());

int offset = packet.getChunkIndex() * ResourcePack.CHUNK_SIZE;
byte[] packData = new byte[(int) MathUtils.constrain(pack.getFile().length() - offset, 0, ResourcePack.CHUNK_SIZE)];

try (InputStream inputStream = new FileInputStream(pack.getFile())) {
inputStream.skip(offset);
inputStream.read(packData, 0, packData.length);
} catch (Exception e) {
e.printStackTrace();
}

data.setData(packData);

session.sendUpstreamPacket(data);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

package org.geysermc.connector.utils;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.geysermc.connector.GeyserConnector;
Expand All @@ -37,6 +39,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.function.Function;

public class FileUtils {
Expand All @@ -55,6 +59,15 @@ public static <T> T loadConfig(File src, Class<T> valueType) throws IOException
return objectMapper.readValue(src, valueType);
}

public static <T> T loadYaml(InputStream src, Class<T> valueType) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()).enable(JsonParser.Feature.IGNORE_UNDEFINED).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return objectMapper.readValue(src, valueType);
}

public static <T> T loadJson(InputStream src, Class<T> valueType) throws IOException {
return GeyserConnector.JSON_MAPPER.readValue(src, valueType);
}

/**
* Open the specified file or copy if from resources
*
Expand Down Expand Up @@ -145,6 +158,23 @@ public static InputStream getResource(String resource) {
return stream;
}

/**
* Calculate the SHA256 hash of the resource pack file
* @param file File to calculate the hash for
* @return A byte[] representation of the hash
*/
public static byte[] calculateSHA256(File file) {
byte[] sha256;

try {
sha256 = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(file.toPath()));
} catch (Exception e) {
throw new RuntimeException("Could not calculate pack hash", e);
}

return sha256;
}

/**
* Get the stored reflection data for a given path
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ public static int ceil(float floatNumber) {
return floatNumber > truncated ? truncated + 1 : truncated;
}

/**
* If number is greater than the max, set it to max, and if number is lower than low, set it to low.
* @param num number to calculate
* @param min the lowest value the number can be
* @param max the greatest value the number can be
* @return - min if num is lower than min <br>
* - max if num is greater than max <br>
* - num otherwise
*/
public static double constrain(double num, double min, double max) {
if (num > max) {
num = max;
}
if (num < min) {
num = min;
}

return num;
}

/**
* Converts the given object from an int or byte to byte.
* This is used for NBT data that might be either an int
Expand Down
114 changes: 114 additions & 0 deletions connector/src/main/java/org/geysermc/connector/utils/ResourcePack.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2019-2020 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/

package org.geysermc.connector.utils;

import org.geysermc.connector.GeyserConnector;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipFile;

/**
* This represents a resource pack and all the data relevant to it
*/
public class ResourcePack {
/**
* The list of loaded resource packs
*/
public static final Map<String, ResourcePack> PACKS = new HashMap<>();

/**
* The size of each chunk to use when sending the resource packs to clients in bytes
*/
public static final int CHUNK_SIZE = 102400;

private byte[] sha256;
private File file;
private ResourcePackManifest manifest;
private ResourcePackManifest.Version version;

/**
* Loop through the packs directory and locate valid resource pack files
*/
public static void loadPacks() {
File directory = GeyserConnector.getInstance().getBootstrap().getConfigFolder().resolve("packs").toFile();

if (!directory.exists()) {
directory.mkdir();

// As we just created the directory it will be empty
return;
}

for (File file : directory.listFiles()) {
if (file.getName().endsWith(".zip") || file.getName().endsWith(".mcpack")) {
ResourcePack pack = new ResourcePack();

pack.sha256 = FileUtils.calculateSHA256(file);

try {
ZipFile zip = new ZipFile(file);

zip.stream().forEach((x) -> {
if (x.getName().contains("manifest.json")) {
try {
ResourcePackManifest manifest = FileUtils.loadJson(zip.getInputStream(x), ResourcePackManifest.class);

pack.file = file;
pack.manifest = manifest;
pack.version = ResourcePackManifest.Version.fromArray(manifest.getHeader().getVersion());

PACKS.put(pack.getManifest().getHeader().getUuid().toString(), pack);
} catch (Exception e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.resource_pack.broken", file.getName()));
e.printStackTrace();
}
}
}
}

public byte[] getSha256() {
return sha256;
}

public File getFile() {
return file;
}

public ResourcePackManifest getManifest() {
return manifest;
}

public ResourcePackManifest.Version getVersion() {
return version;
}
}
Loading

0 comments on commit 99e72f3

Please sign in to comment.