mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
330 lines
12 KiB
Java
330 lines
12 KiB
Java
|
/*
|
||
|
* Copyright (c) 2019-2022 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.geyser.util;
|
||
|
|
||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||
|
import lombok.Getter;
|
||
|
import org.geysermc.geyser.GeyserImpl;
|
||
|
import org.geysermc.geyser.network.GameProtocol;
|
||
|
import org.geysermc.geyser.text.GeyserLocale;
|
||
|
|
||
|
import java.io.*;
|
||
|
import java.nio.file.Files;
|
||
|
import java.nio.file.Path;
|
||
|
import java.util.*;
|
||
|
import java.util.concurrent.CompletableFuture;
|
||
|
import java.util.zip.ZipFile;
|
||
|
|
||
|
/**
|
||
|
* Implementation note: try to design processes to fail softly if the client jar can't be downloaded,
|
||
|
* either if Mojang is down or internet access to Mojang is spotty.
|
||
|
*/
|
||
|
public final class AssetUtils {
|
||
|
private static final String CLIENT_JAR_HASH_FILE = "client_jar.hash";
|
||
|
|
||
|
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
|
||
|
|
||
|
private static VersionDownload CLIENT_JAR_INFO;
|
||
|
|
||
|
private static final Queue<ClientJarTask> CLIENT_JAR_TASKS = new ArrayDeque<>();
|
||
|
/**
|
||
|
* Download the client jar even if the hash is correct
|
||
|
*/
|
||
|
private static boolean FORCE_DOWNLOAD_JAR = false;
|
||
|
|
||
|
public static Asset getAsset(String name) {
|
||
|
return ASSET_MAP.get(name);
|
||
|
}
|
||
|
|
||
|
public static boolean isAssetKnown(String name) {
|
||
|
return ASSET_MAP.containsKey(name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add task to be ran after the client jar is downloaded or found to be cached.
|
||
|
*
|
||
|
* @param required if set to true, the client jar will always be downloaded, even if a pre-existing hash is matched.
|
||
|
* This means an asset or texture is missing.
|
||
|
*/
|
||
|
public static void addTask(boolean required, ClientJarTask task) {
|
||
|
CLIENT_JAR_TASKS.add(task);
|
||
|
FORCE_DOWNLOAD_JAR |= required;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
|
||
|
*/
|
||
|
public static CompletableFuture<Void> generateAssetCache() {
|
||
|
return CompletableFuture.supplyAsync(() -> {
|
||
|
try {
|
||
|
// Get the version manifest from Mojang
|
||
|
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(
|
||
|
WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
|
||
|
|
||
|
// Get the url for the latest version of the games manifest
|
||
|
String latestInfoURL = "";
|
||
|
for (Version version : versionManifest.getVersions()) {
|
||
|
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
|
||
|
latestInfoURL = version.getUrl();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Make sure we definitely got a version
|
||
|
if (latestInfoURL.isEmpty()) {
|
||
|
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
|
||
|
}
|
||
|
|
||
|
// Get the individual version manifest
|
||
|
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
|
||
|
|
||
|
// Get the client jar for use when downloading the en_us locale
|
||
|
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
|
||
|
CLIENT_JAR_INFO = versionInfo.getDownloads().get("client");
|
||
|
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO));
|
||
|
|
||
|
// Get the assets list
|
||
|
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
|
||
|
|
||
|
// Put each asset into an array for use later
|
||
|
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
|
||
|
while (assetIterator.hasNext()) {
|
||
|
Map.Entry<String, JsonNode> entry = assetIterator.next();
|
||
|
if (!entry.getKey().startsWith("minecraft/lang/")) {
|
||
|
// No need to cache non-language assets as we don't use them
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
|
||
|
ASSET_MAP.put(entry.getKey(), asset);
|
||
|
}
|
||
|
|
||
|
} catch (Exception e) {
|
||
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
|
||
|
}
|
||
|
return null;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
public static void downloadAndRunClientJarTasks() {
|
||
|
if (CLIENT_JAR_INFO == null) {
|
||
|
// Likely failed to download
|
||
|
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!FORCE_DOWNLOAD_JAR) { // Don't bother checking the hash if we need to download new files anyway.
|
||
|
String curHash = null;
|
||
|
try {
|
||
|
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve(CLIENT_JAR_HASH_FILE).toFile();
|
||
|
if (hashFile.exists()) {
|
||
|
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
|
||
|
curHash = br.readLine().trim();
|
||
|
}
|
||
|
}
|
||
|
} catch (IOException ignored) { }
|
||
|
String targetHash = CLIENT_JAR_INFO.getSha1();
|
||
|
if (targetHash.equals(curHash)) {
|
||
|
// Just run all tasks - no new download required
|
||
|
ClientJarTask task;
|
||
|
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
|
||
|
task.whenDone.run();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Let the user know we are downloading the JAR
|
||
|
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
|
||
|
GeyserImpl.getInstance().getLogger().debug("Download URL: " + CLIENT_JAR_INFO.getUrl());
|
||
|
|
||
|
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
|
||
|
WebUtils.downloadFile(CLIENT_JAR_INFO.getUrl(), tmpFilePath.toString());
|
||
|
|
||
|
// Load in the JAR as a zip and extract the files
|
||
|
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
|
||
|
ClientJarTask task;
|
||
|
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
|
||
|
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry(task.asset))) {
|
||
|
task.ifNewDownload.accept(fileStream);
|
||
|
task.whenDone.run();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Store the latest jar hash
|
||
|
Path cache = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
|
||
|
Files.createDirectories(cache);
|
||
|
FileUtils.writeFile(cache.resolve(CLIENT_JAR_HASH_FILE).toString(), CLIENT_JAR_INFO.getSha1().toCharArray());
|
||
|
|
||
|
// Delete the nolonger needed client/server jar
|
||
|
Files.delete(tmpFilePath);
|
||
|
|
||
|
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
|
||
|
} catch (Exception e) {
|
||
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void saveFile(File location, InputStream fileStream) throws IOException {
|
||
|
try (FileOutputStream outStream = new FileOutputStream(location)) {
|
||
|
|
||
|
// Write the file to the locale dir
|
||
|
byte[] buf = new byte[fileStream.available()];
|
||
|
int length;
|
||
|
while ((length = fileStream.read(buf)) != -1) {
|
||
|
outStream.write(buf, 0, length);
|
||
|
}
|
||
|
|
||
|
// Flush all changes to disk and cleanup
|
||
|
outStream.flush();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A process that requires we download the client jar.
|
||
|
* Designed to accommodate Geyser updates that require more assets from the jar.
|
||
|
*/
|
||
|
public record ClientJarTask(String asset, InputStreamConsumer ifNewDownload, Runnable whenDone) {
|
||
|
}
|
||
|
|
||
|
@FunctionalInterface
|
||
|
public interface InputStreamConsumer {
|
||
|
void accept(InputStream stream) throws IOException;
|
||
|
}
|
||
|
|
||
|
/* Classes that map to JSON files served by Mojang */
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class VersionManifest {
|
||
|
@JsonProperty("latest")
|
||
|
private LatestVersion latestVersion;
|
||
|
|
||
|
@JsonProperty("versions")
|
||
|
private List<Version> versions;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class LatestVersion {
|
||
|
@JsonProperty("release")
|
||
|
private String release;
|
||
|
|
||
|
@JsonProperty("snapshot")
|
||
|
private String snapshot;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class Version {
|
||
|
@JsonProperty("id")
|
||
|
private String id;
|
||
|
|
||
|
@JsonProperty("type")
|
||
|
private String type;
|
||
|
|
||
|
@JsonProperty("url")
|
||
|
private String url;
|
||
|
|
||
|
@JsonProperty("time")
|
||
|
private String time;
|
||
|
|
||
|
@JsonProperty("releaseTime")
|
||
|
private String releaseTime;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class VersionInfo {
|
||
|
@JsonProperty("id")
|
||
|
private String id;
|
||
|
|
||
|
@JsonProperty("type")
|
||
|
private String type;
|
||
|
|
||
|
@JsonProperty("time")
|
||
|
private String time;
|
||
|
|
||
|
@JsonProperty("releaseTime")
|
||
|
private String releaseTime;
|
||
|
|
||
|
@JsonProperty("assetIndex")
|
||
|
private AssetIndex assetIndex;
|
||
|
|
||
|
@JsonProperty("downloads")
|
||
|
private Map<String, VersionDownload> downloads;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class VersionDownload {
|
||
|
@JsonProperty("sha1")
|
||
|
private String sha1;
|
||
|
|
||
|
@JsonProperty("size")
|
||
|
private int size;
|
||
|
|
||
|
@JsonProperty("url")
|
||
|
private String url;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
static class AssetIndex {
|
||
|
@JsonProperty("id")
|
||
|
private String id;
|
||
|
|
||
|
@JsonProperty("sha1")
|
||
|
private String sha1;
|
||
|
|
||
|
@JsonProperty("size")
|
||
|
private int size;
|
||
|
|
||
|
@JsonProperty("totalSize")
|
||
|
private int totalSize;
|
||
|
|
||
|
@JsonProperty("url")
|
||
|
private String url;
|
||
|
}
|
||
|
|
||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||
|
@Getter
|
||
|
public static class Asset {
|
||
|
@JsonProperty("hash")
|
||
|
private String hash;
|
||
|
|
||
|
@JsonProperty("size")
|
||
|
private int size;
|
||
|
}
|
||
|
|
||
|
private AssetUtils() {
|
||
|
}
|
||
|
}
|