diff --git a/ReadMe.md b/ReadMe.md index 2174c1ebd..31445e28d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -21,8 +21,8 @@ Minosoft is an open source minecraft client, written from scratch in java. It ai - CPU: Minosoft benefits from multiple cores (at least for most stuff). CPU is kind of important, but for the rendering clock speed is more important. 4 cores recommended with at least 2 Ghz. - RAM: Our goal was it to run without compromises on an 8 Gib computer and at least limited on an 4 Gib one. -- Disk space: Minosoft itself is pretty small (2-3 Mib), the libraries are a bit bigger (up to 100 MB). You also need to have the "normal" minecraft assets (~ 300 MB). So a total of 500 MB is recommended. - - GPU: Currently only needed for rendering, no clue yet. +- Disk space: Minosoft itself is pretty small (2-3 Mib), the libraries are a bit bigger. You also need to have the "normal" minecraft assets (~ 300 MB for one version). Assets never get saved duplicated, so if you use multiple versions, it is not a multiple of that value! If you only play 1 version, 500 MB is recommended. If not, use at least 1 GB. +- GPU: Currently only needed for rendering, no clue yet. - Java 15 (This is really important, we use specific (experimental) features that are only available in the latest version. Java 8 is **not** supported). OpenJDK is (of course) also supported. diff --git a/doc/Assets.md b/doc/Assets.md index 93bc7fdc4..90b48e662 100644 --- a/doc/Assets.md +++ b/doc/Assets.md @@ -25,8 +25,10 @@ If not, we will download it and store it gzip compressed. ## Valid (Relevant) files Before downloading a file, the file is checked for relevance. Relevant files are prefixed with the following strings (or the file path): - - `minecraft/lang/` -> Language files - - `minecraft/sounds/` -> Sounds + +- `minecraft/lang/` -> Language files +- `minecraft/sounds.json` -> Sound meta data and index file +- `minecraft/sounds/` -> Sounds - `minecraft/textures/` -> Textures - `minecraft/font/` -> Fonts diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.java b/src/main/java/de/bixilon/minosoft/Minosoft.java index 37f407ab4..44e3b5831 100644 --- a/src/main/java/de/bixilon/minosoft/Minosoft.java +++ b/src/main/java/de/bixilon/minosoft/Minosoft.java @@ -20,9 +20,8 @@ import de.bixilon.minosoft.config.Configuration; import de.bixilon.minosoft.config.ConfigurationPaths; import de.bixilon.minosoft.config.StaticConfiguration; import de.bixilon.minosoft.data.accounts.Account; -import de.bixilon.minosoft.data.assets.AssetsManager; +import de.bixilon.minosoft.data.assets.Resources; import de.bixilon.minosoft.data.locale.LocaleManager; -import de.bixilon.minosoft.data.locale.minecraft.MinecraftLocaleManager; import de.bixilon.minosoft.data.mappings.versions.Versions; import de.bixilon.minosoft.gui.main.GUITools; import de.bixilon.minosoft.gui.main.Launcher; @@ -130,6 +129,8 @@ public final class Minosoft { long mappingStartLoadingTime = System.currentTimeMillis(); Versions.loadAvailableVersions(Util.readJsonAsset("mapping/versions.json")); Log.info(String.format("Loaded %d versions in %dms", Versions.getVersionIdMap().size(), (System.currentTimeMillis() - mappingStartLoadingTime))); + Resources.load(); + Log.info("Loaded all resources!"); }, "Version mappings", "Load available minecraft versions inclusive mappings", Priorities.NORMAL, TaskImportance.REQUIRED, "Configuration")); taskWorker.addTask(new Task(progress -> { @@ -140,10 +141,6 @@ public final class Minosoft { taskWorker.addTask(new Task(ModLoader::loadMods, "ModLoading", "Load all minosoft mods", Priorities.NORMAL, TaskImportance.REQUIRED, "Configuration")); - taskWorker.addTask(new Task(AssetsManager::downloadAllAssets, "Assets", "Download and verify all minecraft assets", Priorities.HIGH, TaskImportance.REQUIRED, "Configuration")); - - taskWorker.addTask(new Task(progress -> MinecraftLocaleManager.load(config.getString(ConfigurationPaths.StringPaths.GENERAL_LANGUAGE)), "Mojang language", "Load minecraft language files", Priorities.HIGH, TaskImportance.REQUIRED, "Assets")); - taskWorker.addTask(new Task(progress -> { if (!config.getBoolean(ConfigurationPaths.BooleanPaths.NETWORK_SHOW_LAN_SERVERS)) { return; @@ -151,7 +148,7 @@ public final class Minosoft { LANServerListener.listen(); }, "LAN Server Listener", "Listener for LAN Servers", Priorities.LOWEST, TaskImportance.OPTIONAL, "Configuration")); - taskWorker.addTask(new Task(progress -> CLI.initialize(), "CLI", "Initialize CLI", Priorities.LOW, TaskImportance.OPTIONAL, "Assets", "Mojang language")); + taskWorker.addTask(new Task(progress -> CLI.initialize(), "CLI", "Initialize CLI", Priorities.LOW, TaskImportance.OPTIONAL)); if (!StaticConfiguration.HEADLESS_MODE) { taskWorker.addTask(new Task((progress) -> StartProgressWindow.start(), "JavaFX Toolkit", "Initialize JavaFX", Priorities.HIGHEST)); diff --git a/src/main/java/de/bixilon/minosoft/data/Mappings.java b/src/main/java/de/bixilon/minosoft/data/Mappings.java index 5ba3b6f81..a700eb975 100644 --- a/src/main/java/de/bixilon/minosoft/data/Mappings.java +++ b/src/main/java/de/bixilon/minosoft/data/Mappings.java @@ -18,6 +18,7 @@ public enum Mappings { BLOCKS, ENTITIES; + public static final Mappings[] VALUES = values(); public String getFilename() { return name().toLowerCase(); } diff --git a/src/main/java/de/bixilon/minosoft/data/assets/AssetVersion.kt b/src/main/java/de/bixilon/minosoft/data/assets/AssetVersion.kt new file mode 100644 index 000000000..09f3a24b7 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/data/assets/AssetVersion.kt @@ -0,0 +1,12 @@ +package de.bixilon.minosoft.data.assets + +import de.bixilon.minosoft.data.mappings.versions.Version + +data class AssetVersion( + val version: Version, + val indexVersion: String?, + val indexHash: String?, + val clientJarHash: String?, + val jarAssetsHash: String?, + val minosoftMappings: String? +) diff --git a/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java b/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java index c98ce9bac..f7b82a848 100644 --- a/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java +++ b/src/main/java/de/bixilon/minosoft/data/assets/AssetsManager.java @@ -13,7 +13,6 @@ package de.bixilon.minosoft.data.assets; -import com.google.errorprone.annotations.DoNotCall; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -23,10 +22,8 @@ import de.bixilon.minosoft.config.ConfigurationPaths; import de.bixilon.minosoft.config.StaticConfiguration; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; import de.bixilon.minosoft.util.CountUpAndDownLatch; -import de.bixilon.minosoft.util.HTTP; import de.bixilon.minosoft.util.Util; import de.bixilon.minosoft.util.logging.Log; -import de.bixilon.minosoft.util.logging.LogLevels; import java.io.*; import java.security.MessageDigest; @@ -38,114 +35,25 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class AssetsManager { - public static final String ASSETS_INDEX_VERSION = "1.17"; // version.json -> assetIndex -> id - public static final String ASSETS_INDEX_HASH = "e022240e3d70866f41dd88a3b342cf842a7b31bd"; // version.json -> assetIndex -> sha1 - public static final String ASSETS_CLIENT_JAR_VERSION = "20w51a"; // version.json -> id - public static final String ASSETS_CLIENT_JAR_HASH = "1e4d125252481d833930371c9827d72d0fb35cfd"; // sha1 hash of file generated by minosoft (client jar file mappings: name -> hash) - public static final String[] RELEVANT_ASSETS = {"minecraft/lang/", "minecraft/sounds/", "minecraft/textures/", "minecraft/font/"}; + private static final String[] RELEVANT_ASSETS = {"minecraft/lang/", "minecraft/sounds.json", "minecraft/sounds/", "minecraft/textures/", "minecraft/font/"}; // whitelist for all assets we care (we have our own block models, etc) + private final boolean verifyHash; + private final AssetVersion assetVersion; + private final HashMap assetsMap = new HashMap<>(); - private static final HashMap ASSETS_MAP = new HashMap<>(); - - public static void downloadAssetsIndex() throws IOException { - Util.downloadFileAsGz(String.format("https://launchermeta.mojang.com/v1/packages/%s/%s.json", ASSETS_INDEX_HASH, ASSETS_INDEX_VERSION), getAssetDiskPath(ASSETS_INDEX_HASH)); + public AssetsManager(boolean verifyHash, AssetVersion assetVersion) { + this.verifyHash = verifyHash; + this.assetVersion = assetVersion; } - private static HashMap parseAssetsIndex(String hash) throws IOException { - return parseAssetsIndex(readJsonAssetByHash(hash).getAsJsonObject()); - } - - private static HashMap parseAssetsIndex(JsonObject json) { - if (json.has("objects")) { - json = json.getAsJsonObject("objects"); - } - HashMap ret = new HashMap<>(); - for (String key : json.keySet()) { - JsonElement value = json.get(key); - if (value.isJsonPrimitive()) { - ret.put(key, value.getAsString()); - continue; - } - ret.put(key, value.getAsJsonObject().get("hash").getAsString()); - } - return ret; - } - - public static void downloadAllAssets(CountUpAndDownLatch latch) throws IOException, InterruptedException { - if (!ASSETS_MAP.isEmpty()) { - return; - } - try { - downloadAssetsIndex(); - } catch (Exception e) { - Log.printException(e, LogLevels.DEBUG); - Log.warn("Could not download assets index. Please check your internet connection"); - } - ASSETS_MAP.putAll(verifyAssets(AssetsSource.MOJANG, latch, parseAssetsIndex(ASSETS_INDEX_HASH))); - ASSETS_MAP.putAll(verifyAssets(AssetsSource.MINOSOFT_GIT, latch, parseAssetsIndex(Util.readJsonAsset("mapping/resources.json")))); - latch.addCount(1); // client jar - // download assets - generateJarAssets(); - ASSETS_MAP.putAll(parseAssetsIndex(ASSETS_CLIENT_JAR_HASH)); - latch.countDown(); - } - - private static HashMap verifyAssets(AssetsSource source, CountUpAndDownLatch latch, HashMap assets) { - latch.addCount(assets.size()); - assets.keySet().parallelStream().forEach((filename) -> { - try { - String hash = assets.get(filename); - boolean compressed = (source == AssetsSource.MOJANG); - if (StaticConfiguration.DEBUG_SLOW_LOADING) { - Thread.sleep(100L); - } - if (!verifyAssetHash(hash, compressed)) { - downloadAsset(source, hash); - } - latch.countDown(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - }); - return assets; - } - - public static boolean doesAssetExist(String name) { - return ASSETS_MAP.containsKey(name); - } - - public static HashMap getAssetsMap() { - return ASSETS_MAP; - } - - public static InputStreamReader readAsset(String name) throws IOException { - return readAssetByHash(ASSETS_MAP.get(name)); - } - - public static InputStream readAssetAsStream(String name) throws IOException { - return readAssetAsStreamByHash(ASSETS_MAP.get(name)); - } - - public static JsonElement readJsonAsset(String name) throws IOException { - return readJsonAssetByHash(ASSETS_MAP.get(name)); - } - - private static void downloadAsset(AssetsSource source, String hash) throws Exception { - switch (source) { - case MOJANG -> downloadAsset(String.format(ProtocolDefinition.MOJANG_URL_RESOURCES, hash.substring(0, 2), hash), hash); - case MINOSOFT_GIT -> downloadAsset(String.format(Minosoft.getConfig().getString(ConfigurationPaths.StringPaths.RESOURCES_URL), hash.substring(0, 2), hash), hash, false); - } - } - - private static InputStreamReader readAssetByHash(String hash) throws IOException { + public static InputStreamReader readAssetByHash(String hash) throws IOException { return new InputStreamReader(readAssetAsStreamByHash(hash)); } - private static InputStream readAssetAsStreamByHash(String hash) throws IOException { + public static InputStream readAssetAsStreamByHash(String hash) throws IOException { return new GZIPInputStream(new FileInputStream(getAssetDiskPath(hash))); } - private static JsonElement readJsonAssetByHash(String hash) throws IOException { + public static JsonElement readJsonAssetByHash(String hash) throws IOException { return JsonParser.parseReader(readAssetByHash(hash)); } @@ -157,89 +65,6 @@ public class AssetsManager { return file.length(); } - private static boolean verifyAssetHash(String hash, boolean compressed) throws FileNotFoundException { - // file does not exist - if (getAssetSize(hash) == -1) { - return false; - } - if (!Minosoft.getConfig().getBoolean(ConfigurationPaths.BooleanPaths.DEBUG_VERIFY_ASSETS)) { - return true; - } - try { - if (compressed) { - return hash.equals(Util.sha1Gzip(new File(getAssetDiskPath(hash)))); - } - return hash.equals(Util.sha1(new File(getAssetDiskPath(hash)))); - } catch (IOException ignored) { - } - return false; - } - - private static boolean verifyAssetHash(String hash) { - try { - return verifyAssetHash(hash, true); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - return false; - } - - public static void generateJarAssets() throws IOException, InterruptedException { - long startTime = System.currentTimeMillis(); - Log.verbose("Generating client.jar assets..."); - if (verifyAssetHash(ASSETS_CLIENT_JAR_HASH)) { - // ToDo: Verify all jar assets - readAssetAsStreamByHash(ASSETS_CLIENT_JAR_HASH); - Log.verbose("client.jar assets probably already generated, skipping"); - return; - } - JsonObject manifest = HTTP.getJson(ProtocolDefinition.MOJANG_URL_VERSION_MANIFEST).getAsJsonObject(); - String assetsVersionJsonUrl = null; - for (JsonElement versionElement : manifest.getAsJsonArray("versions")) { - JsonObject version = versionElement.getAsJsonObject(); - if (version.get("id").getAsString().equals(ASSETS_CLIENT_JAR_VERSION)) { - assetsVersionJsonUrl = version.get("url").getAsString(); - break; - } - } - if (assetsVersionJsonUrl == null) { - throw new RuntimeException(String.format("Invalid version manifest or invalid ASSETS_CLIENT_JAR_VERSION (%s)", ASSETS_CLIENT_JAR_VERSION)); - } - String versionJsonHash = assetsVersionJsonUrl.replace(ProtocolDefinition.MOJANG_URL_PACKAGES, "").replace(String.format("/%s.json", ASSETS_CLIENT_JAR_VERSION), ""); - downloadAsset(assetsVersionJsonUrl, versionJsonHash); - // download jar - JsonObject clientJarJson = readJsonAssetByHash(versionJsonHash).getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("client"); - downloadAsset(clientJarJson.get("url").getAsString(), clientJarJson.get("sha1").getAsString()); - - HashMap clientJarAssetsHashMap = new HashMap<>(); - ZipInputStream versionJar = new ZipInputStream(readAssetAsStreamByHash(clientJarJson.get("sha1").getAsString())); - ZipEntry currentFile; - while ((currentFile = versionJar.getNextEntry()) != null) { - if (!currentFile.getName().startsWith("assets") || currentFile.isDirectory()) { - continue; - } - boolean relevant = false; - for (String prefix : RELEVANT_ASSETS) { - if (currentFile.getName().startsWith("assets/" + prefix)) { - relevant = true; - break; - } - } - if (!relevant) { - continue; - } - String hash = saveAsset(versionJar); - - clientJarAssetsHashMap.put(currentFile.getName().substring("assets/".length()), hash); - } - JsonObject clientJarAssetsMapping = new JsonObject(); - clientJarAssetsHashMap.forEach(clientJarAssetsMapping::addProperty); - String json = new GsonBuilder().create().toJson(clientJarAssetsMapping); - String assetHash = saveAsset(json.getBytes()); - Log.verbose(String.format("Generated jar assets in %dms (elements=%d, hash=%s)", (System.currentTimeMillis() - startTime), clientJarAssetsHashMap.size(), assetHash)); - } - - @DoNotCall private static String saveAsset(byte[] data) throws IOException { String hash = Util.sha1(data); String destination = getAssetDiskPath(hash); @@ -254,6 +79,13 @@ public class AssetsManager { return hash; } + /** + * Downloads/Copies an asset from a given stream + * + * @param data: Data to save + * @return SHA-1 hash of file + * @throws IOException On error + */ private static String saveAsset(InputStream data) throws IOException { File tempDestinationFile = null; while (tempDestinationFile == null || tempDestinationFile.exists()) { // file exist? lol @@ -295,11 +127,45 @@ public class AssetsManager { return hash; } - private static void downloadAsset(String url, String hash) throws IOException { + private static String getAssetDiskPath(String hash) throws FileNotFoundException { + if (hash == null) { + throw new FileNotFoundException("Could not find asset with hash: null"); + } + return StaticConfiguration.HOME_DIRECTORY + String.format("assets/objects/%s/%s.gz", hash.substring(0, 2), hash); + } + + private boolean verifyAssetHash(String hash, boolean compressed) throws FileNotFoundException { + // file does not exist + if (getAssetSize(hash) == -1) { + return false; + } + if (!this.verifyHash) { + return true; + } + try { + if (compressed) { + return hash.equals(Util.sha1Gzip(new File(getAssetDiskPath(hash)))); + } + return hash.equals(Util.sha1(new File(getAssetDiskPath(hash)))); + } catch (IOException ignored) { + } + return false; + } + + private boolean verifyAssetHash(String hash) { + try { + return verifyAssetHash(hash, true); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return false; + } + + private void downloadAsset(String url, String hash) throws IOException { downloadAsset(url, hash, true); } - private static void downloadAsset(String url, String hash, boolean compressed) throws IOException { + private void downloadAsset(String url, String hash, boolean compressed) throws IOException { if (verifyAssetHash(hash)) { return; } @@ -312,10 +178,137 @@ public class AssetsManager { Util.downloadFile(url, getAssetDiskPath(hash)); } - private static String getAssetDiskPath(String hash) throws FileNotFoundException { - if (hash == null) { - throw new FileNotFoundException("Could not find asset with hash: null"); + public void downloadAssetsIndex() throws IOException { + Util.downloadFileAsGz(String.format(ProtocolDefinition.MOJANG_URL_PACKAGES + ".json", this.assetVersion.getIndexHash(), this.assetVersion.getIndexVersion()), getAssetDiskPath(this.assetVersion.getIndexHash())); + } + + private HashMap parseAssetsIndex(String hash) throws IOException { + return parseAssetsIndex(readJsonAssetByHash(hash).getAsJsonObject()); + } + + private HashMap parseAssetsIndex(JsonObject json) { + if (json.has("objects")) { + json = json.getAsJsonObject("objects"); } - return StaticConfiguration.HOME_DIRECTORY + String.format("assets/objects/%s/%s.gz", hash.substring(0, 2), hash); + HashMap ret = new HashMap<>(); + for (String key : json.keySet()) { + JsonElement value = json.get(key); + if (value.isJsonPrimitive()) { + ret.put(key, value.getAsString()); + continue; + } + ret.put(key, value.getAsJsonObject().get("hash").getAsString()); + } + return ret; + } + + public void downloadAllAssets(CountUpAndDownLatch latch) throws Exception { + if (!this.assetsMap.isEmpty()) { + return; + } + // download minecraft assets + if (!doesAssetExist(this.assetVersion.getIndexHash())) { + downloadAssetsIndex(); + } + this.assetsMap.putAll(verifyAssets(AssetsSource.MOJANG, latch, parseAssetsIndex(this.assetVersion.getIndexHash()))); + + // generate jar assets index + generateJarAssets(); + + // download minosoft mappings + + downloadAsset(AssetsSource.MINOSOFT_GIT, this.assetVersion.getMinosoftMappings()); + } + + private HashMap verifyAssets(AssetsSource source, CountUpAndDownLatch latch, HashMap assets) { + latch.addCount(assets.size()); + assets.keySet().parallelStream().forEach((filename) -> { + try { + String hash = assets.get(filename); + boolean compressed = (source == AssetsSource.MOJANG); + if (StaticConfiguration.DEBUG_SLOW_LOADING) { + Thread.sleep(100L); + } + if (!verifyAssetHash(hash, compressed)) { + downloadAsset(source, hash); + } + latch.countDown(); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + }); + return assets; + } + + private void downloadAsset(AssetsSource source, String hash) throws IOException { + switch (source) { + case MINECRAFT -> downloadAsset(String.format(ProtocolDefinition.MINECRAFT_URL_RESOURCES, hash.substring(0, 2), hash), hash); + case MINOSOFT_GIT -> downloadAsset(String.format(Minosoft.getConfig().getString(ConfigurationPaths.StringPaths.RESOURCES_URL), hash.substring(0, 2), hash), hash, false); + } + } + + public String generateJarAssets() throws IOException { + long startTime = System.currentTimeMillis(); + Log.verbose("Generating client.jar assets for %s...", this.assetVersion.getVersion()); + if (verifyAssetHash(this.assetVersion.getJarAssetsHash())) { + // ToDo: Verify all jar assets + Log.verbose("client.jar assets probably already generated for %s, skipping", this.assetVersion.getVersion()); + return this.assetVersion.getJarAssetsHash(); + } + // download jar + downloadAsset(String.format(ProtocolDefinition.MOJANG_LAUNCHER_URL_PACKAGES, this.assetVersion.getClientJarHash(), "client.jar"), this.assetVersion.getClientJarHash()); + + HashMap clientJarAssetsHashMap = new HashMap<>(); + ZipInputStream versionJar = new ZipInputStream(readAssetAsStreamByHash(this.assetVersion.getClientJarHash())); + ZipEntry currentFile; + while ((currentFile = versionJar.getNextEntry()) != null) { + if (!currentFile.getName().startsWith("assets") || currentFile.isDirectory()) { + continue; + } + boolean relevant = false; + for (String prefix : RELEVANT_ASSETS) { + if (currentFile.getName().startsWith("assets/" + prefix)) { + relevant = true; + break; + } + } + if (!relevant) { + continue; + } + String hash = saveAsset(versionJar); + + clientJarAssetsHashMap.put(currentFile.getName().substring("assets/".length()), hash); + } + JsonObject clientJarAssetsMapping = new JsonObject(); + clientJarAssetsHashMap.forEach(clientJarAssetsMapping::addProperty); + String json = new GsonBuilder().create().toJson(clientJarAssetsMapping); + String assetHash = saveAsset(json.getBytes()); + Log.verbose(String.format("Generated jar assets in %dms (elements=%d, hash=%s)", (System.currentTimeMillis() - startTime), clientJarAssetsHashMap.size(), assetHash)); + return assetHash; + } + + public boolean doesAssetExist(String name) { + return this.assetsMap.containsKey(name); + } + + public HashMap getAssetsMap() { + return this.assetsMap; + } + + public InputStreamReader readAsset(String name) throws IOException { + return readAssetByHash(this.assetsMap.get(name)); + } + + public InputStream readAssetAsStream(String name) throws IOException { + return readAssetAsStreamByHash(this.assetsMap.get(name)); + } + + public JsonElement readJsonAsset(String name) throws IOException { + return readJsonAssetByHash(this.assetsMap.get(name)); + } + + public AssetVersion getAssetVersion() { + return this.assetVersion; } } diff --git a/src/main/java/de/bixilon/minosoft/data/assets/AssetsSource.java b/src/main/java/de/bixilon/minosoft/data/assets/AssetsSource.java index 2d19dbb54..a2277c897 100644 --- a/src/main/java/de/bixilon/minosoft/data/assets/AssetsSource.java +++ b/src/main/java/de/bixilon/minosoft/data/assets/AssetsSource.java @@ -15,5 +15,6 @@ package de.bixilon.minosoft.data.assets; public enum AssetsSource { MOJANG, + MINECRAFT, MINOSOFT_GIT } diff --git a/src/main/java/de/bixilon/minosoft/data/assets/Resources.java b/src/main/java/de/bixilon/minosoft/data/assets/Resources.java new file mode 100644 index 000000000..a4e2260fa --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/data/assets/Resources.java @@ -0,0 +1,44 @@ +package de.bixilon.minosoft.data.assets; + +import com.google.common.collect.HashBiMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.bixilon.minosoft.data.mappings.versions.Version; +import de.bixilon.minosoft.data.mappings.versions.Versions; +import de.bixilon.minosoft.util.Util; + +import java.io.IOException; +import java.util.Map; + +public class Resources { + private static final HashBiMap ASSETS_VERSIONS = HashBiMap.create(); + + public static void load() throws IOException { + JsonObject json = Util.readJsonAsset("mapping/resources.json"); + + JsonObject versions = json.getAsJsonObject("versions"); + for (Map.Entry versionEntry : versions.entrySet()) { + loadVersion(versionEntry.getKey(), versionEntry.getValue().getAsJsonObject()); + } + + } + + public static void loadVersion(String versionName, JsonObject json) { + loadVersion(Versions.getVersionByName(versionName), json); + } + + public static void loadVersion(Version version, JsonObject json) { + String indexVersion = json.has("index_version") ? json.get("index_version").getAsString() : null; + String indexHash = json.has("index_hash") ? json.get("index_hash").getAsString() : null; + String clientJarHash = json.has("client_jar_hash") ? json.get("client_jar_hash").getAsString() : null; + String jarAssetsHash = json.has("jar_assets_hash") ? json.get("jar_assets_hash").getAsString() : null; + String minosoftMapping = json.has("mappings") ? json.get("mappings").getAsString() : null; + + AssetVersion assetVersion = new AssetVersion(version, indexVersion, indexHash, clientJarHash, jarAssetsHash, minosoftMapping); + ASSETS_VERSIONS.put(version, assetVersion); + } + + public static AssetVersion getAssetVersionByVersion(Version version) { + return ASSETS_VERSIONS.get(version); + } +} diff --git a/src/main/java/de/bixilon/minosoft/data/commands/parser/ComponentParser.kt b/src/main/java/de/bixilon/minosoft/data/commands/parser/ComponentParser.kt index d11b50713..491792551 100644 --- a/src/main/java/de/bixilon/minosoft/data/commands/parser/ComponentParser.kt +++ b/src/main/java/de/bixilon/minosoft/data/commands/parser/ComponentParser.kt @@ -24,7 +24,7 @@ class ComponentParser : CommandParser() { @Throws(CommandParseException::class) override fun parse(connection: Connection, properties: ParserProperties?, stringReader: CommandStringReader): Any? { try { - return BaseComponent(stringReader.readJson().asJsonObject) + return BaseComponent(connection.version, stringReader.readJson().asJsonObject) } catch (exception: Exception) { stringReader.skip(-1) throw InvalidComponentCommandParseException(stringReader, stringReader.read().toString(), exception) diff --git a/src/main/java/de/bixilon/minosoft/data/commands/parser/ItemStackParser.kt b/src/main/java/de/bixilon/minosoft/data/commands/parser/ItemStackParser.kt index 7d6dc4f7a..291e00c7a 100644 --- a/src/main/java/de/bixilon/minosoft/data/commands/parser/ItemStackParser.kt +++ b/src/main/java/de/bixilon/minosoft/data/commands/parser/ItemStackParser.kt @@ -40,7 +40,7 @@ class ItemStackParser : CommandParser() { if (stringReader.peek() == '{') { nbt = stringReader.readNBTCompoundTag() } - return Slot(connection.mapping, Item(argument.value.mod, argument.value.identifier), 1, nbt) + return Slot(connection.version, Item(argument.value.mod, argument.value.identifier), 1, nbt) } companion object { diff --git a/src/main/java/de/bixilon/minosoft/data/entities/block/CampfireBlockEntityMetaData.java b/src/main/java/de/bixilon/minosoft/data/entities/block/CampfireBlockEntityMetaData.java index cd365edcd..f90a027e1 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/block/CampfireBlockEntityMetaData.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/block/CampfireBlockEntityMetaData.java @@ -28,7 +28,7 @@ public class CampfireBlockEntityMetaData extends BlockEntityMetaData { public CampfireBlockEntityMetaData(ListTag nbt) { this.items = new Slot[4]; for (CompoundTag tag : nbt.getValue()) { - this.items[tag.getByteTag("Slot").getValue()] = new Slot(new Item(tag.getStringTag("id").getValue()), tag.getByteTag("Count").getValue()); + this.items[tag.getByteTag("Slot").getValue()] = new Slot(null, new Item(tag.getStringTag("id").getValue()), tag.getByteTag("Count").getValue()); // ToDo: version should not be null } } diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/LargeFireball.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/LargeFireball.java index 16826d6ac..56c67d2ba 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/LargeFireball.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/LargeFireball.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class LargeFireball extends Fireball { - private static final Slot DEFAULT_ITEM = new Slot(new Item("fire_charge")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("fire_charge")); // ToDo: version should not be null public LargeFireball(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/SmallFireball.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/SmallFireball.java index daba1255e..e8cad9f33 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/SmallFireball.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/SmallFireball.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class SmallFireball extends Fireball { - private static final Slot DEFAULT_ITEM = new Slot(new Item("fire_charge")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("fire_charge")); // ToDo: version should not be null public SmallFireball(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEgg.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEgg.java index 18dd993d9..5a64824ae 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEgg.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEgg.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class ThrownEgg extends ThrowableItemProjectile { - private static final Slot DEFAULT_ITEM = new Slot(new Item("egg")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("egg")); // ToDo: version should not be null public ThrownEgg(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEnderPearl.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEnderPearl.java index 9425359b9..1ba806e34 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEnderPearl.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEnderPearl.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class ThrownEnderPearl extends ThrowableItemProjectile { - private static final Slot DEFAULT_ITEM = new Slot(new Item("ender_pearl")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("ender_pearl")); // ToDo: version should not be null public ThrownEnderPearl(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownExperienceBottle.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownExperienceBottle.java index 8da8e3c00..af78f9aaf 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownExperienceBottle.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownExperienceBottle.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class ThrownExperienceBottle extends ThrowableItemProjectile { - private static final Slot DEFAULT_ITEM = new Slot(new Item("experience_bottle")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("experience_bottle")); // ToDo: version should not be null public ThrownExperienceBottle(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEyeOfEnder.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEyeOfEnder.java index 6792c92f9..762f762b5 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEyeOfEnder.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownEyeOfEnder.java @@ -25,7 +25,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class ThrownEyeOfEnder extends Entity { - private static final Slot DEFAULT_ITEM = new Slot(new Item("ender_eye")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("ender_eye")); // ToDo: version should not be null public ThrownEyeOfEnder(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownSnowball.java b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownSnowball.java index ffd69b85a..ff5c27fbf 100644 --- a/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownSnowball.java +++ b/src/main/java/de/bixilon/minosoft/data/entities/entities/projectile/ThrownSnowball.java @@ -22,7 +22,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import java.util.UUID; public class ThrownSnowball extends ThrowableItemProjectile { - private static final Slot DEFAULT_ITEM = new Slot(new Item("snowball")); + private static final Slot DEFAULT_ITEM = new Slot(null, new Item("snowball")); // ToDo: version should not be null public ThrownSnowball(Connection connection, int entityId, UUID uuid, Location location, EntityRotation rotation) { super(connection, entityId, uuid, location, rotation); diff --git a/src/main/java/de/bixilon/minosoft/data/inventory/Slot.java b/src/main/java/de/bixilon/minosoft/data/inventory/Slot.java index 5ebbab162..0266bb38d 100644 --- a/src/main/java/de/bixilon/minosoft/data/inventory/Slot.java +++ b/src/main/java/de/bixilon/minosoft/data/inventory/Slot.java @@ -13,9 +13,9 @@ package de.bixilon.minosoft.data.inventory; -import de.bixilon.minosoft.data.locale.minecraft.MinecraftLocaleManager; import de.bixilon.minosoft.data.mappings.Enchantment; import de.bixilon.minosoft.data.mappings.Item; +import de.bixilon.minosoft.data.mappings.versions.Version; import de.bixilon.minosoft.data.mappings.versions.VersionMapping; import de.bixilon.minosoft.data.text.ChatComponent; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; @@ -31,6 +31,7 @@ public class Slot { private final Item item; private final HashMap enchantments = new HashMap<>(); private final ArrayList lore = new ArrayList<>(); + private final Version version; int itemCount; short itemMetadata; int repairCost; @@ -41,20 +42,21 @@ public class Slot { byte hideFlags; - public Slot(VersionMapping mapping, Item item, int itemCount, CompoundTag nbt) { - this(item); + public Slot(Version version, Item item, int itemCount, CompoundTag nbt) { + this(version, item); this.itemCount = itemCount; - setNBT(mapping, nbt); + setNBT(nbt); } - public Slot(VersionMapping mapping, Item item, byte itemCount, short itemMetadata, CompoundTag nbt) { - this(item); + public Slot(Version version, Item item, byte itemCount, short itemMetadata, CompoundTag nbt) { + this(version, item); this.itemMetadata = itemMetadata; this.itemCount = itemCount; - setNBT(mapping, nbt); + setNBT(nbt); } - public Slot(Item item) { + public Slot(Version version, Item item) { + this.version = version; if (item.getFullIdentifier().equals("minecraft:air")) { this.item = null; } else { @@ -62,12 +64,12 @@ public class Slot { } } - public Slot(Item item, byte itemCount) { - this(item); + public Slot(Version version, Item item, byte itemCount) { + this(version, item); this.itemCount = itemCount; } - private void setNBT(VersionMapping mapping, CompoundTag nbt) { + private void setNBT(CompoundTag nbt) { if (nbt == null) { return; } @@ -77,11 +79,11 @@ public class Slot { if (nbt.containsKey("display")) { CompoundTag display = nbt.getCompoundTag("display"); if (display.containsKey("Name")) { - this.customDisplayName = ChatComponent.valueOf(display.getStringTag("Name").getValue()); + this.customDisplayName = ChatComponent.valueOf(this.version, display.getStringTag("Name").getValue()); } if (display.containsKey("Lore")) { for (StringTag lore : display.getListTag("Lore").getValue()) { - this.lore.add(ChatComponent.valueOf(lore.getValue())); + this.lore.add(ChatComponent.valueOf(this.version, lore.getValue())); } } } @@ -101,7 +103,7 @@ public class Slot { } } else if (nbt.containsKey("ench")) { for (CompoundTag enchantment : nbt.getListTag("ench").getValue()) { - this.enchantments.put(mapping.getEnchantmentById(enchantment.getNumberTag("id").getAsInt()), enchantment.getNumberTag("lvl").getAsInt()); + this.enchantments.put(this.version.getMapping().getEnchantmentById(enchantment.getNumberTag("id").getAsInt()), enchantment.getNumberTag("lvl").getAsInt()); } } } @@ -244,8 +246,8 @@ public class Slot { // ToDo: What if an item identifier changed between versions? oOo String[] keys = {String.format("item.%s.%s", this.item.getMod(), this.item.getIdentifier()), String.format("block.%s.%s", this.item.getMod(), this.item.getIdentifier())}; for (String key : keys) { - if (MinecraftLocaleManager.getLanguage().canTranslate(key)) { - return MinecraftLocaleManager.translate(key); + if (this.version.getLocaleManager().canTranslate(key)) { + return this.version.getLocaleManager().translate(key); } } return this.item.getFullIdentifier(); diff --git a/src/main/java/de/bixilon/minosoft/data/locale/minecraft/MinecraftLocaleManager.java b/src/main/java/de/bixilon/minosoft/data/locale/minecraft/MinecraftLocaleManager.java index 4b23eca86..ca1ea182a 100644 --- a/src/main/java/de/bixilon/minosoft/data/locale/minecraft/MinecraftLocaleManager.java +++ b/src/main/java/de/bixilon/minosoft/data/locale/minecraft/MinecraftLocaleManager.java @@ -13,35 +13,44 @@ package de.bixilon.minosoft.data.locale.minecraft; -import de.bixilon.minosoft.data.assets.AssetsManager; +import de.bixilon.minosoft.data.mappings.versions.Version; import de.bixilon.minosoft.util.logging.Log; import java.io.IOException; public class MinecraftLocaleManager { - private static MinecraftLanguage language; + private final Version version; + private MinecraftLanguage language; - public static MinecraftLanguage getLanguage() { - return language; + public MinecraftLocaleManager(Version version) { + this.version = version; } - public static String translate(String key, Object... data) { - return language.translate(key, data); + public MinecraftLanguage getLanguage() { + return this.language; } - private static MinecraftLanguage loadLanguage(String language) throws IOException { - return new MinecraftLanguage(language, AssetsManager.readJsonAsset(String.format("minecraft/lang/%s.json", language.toLowerCase())).getAsJsonObject()); + public String translate(String key, Object... data) { + return this.language.translate(key, data); } - public static void load(String language) { + private MinecraftLanguage loadLanguage(String language) throws IOException { + return new MinecraftLanguage(language, this.version.getAssetsManager().readJsonAsset(String.format("minecraft/lang/%s.json", language.toLowerCase())).getAsJsonObject()); + } + + public void load(String language) { long startTime = System.currentTimeMillis(); - Log.verbose(String.format("Loading minecraft language file (%s)", language)); + Log.verbose(String.format("Loading minecraft language file (%s) for %s", language, this.version)); try { - MinecraftLocaleManager.language = loadLanguage(language); + this.language = loadLanguage(language); } catch (Exception e) { e.printStackTrace(); - Log.warn(String.format("Could not load minecraft language file: %s", language)); + Log.warn("Could not load minecraft language file: %s for %s", language, this.version); } - Log.verbose(String.format("Loaded minecraft language files successfully in %dms", (System.currentTimeMillis() - startTime))); + Log.verbose("Loaded minecraft language files for %s successfully in %dms", this.version, (System.currentTimeMillis() - startTime)); + } + + public boolean canTranslate(String key) { + return getLanguage().canTranslate(key); } } diff --git a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Version.java b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Version.java index 56b64f650..658fd884e 100644 --- a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Version.java +++ b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Version.java @@ -14,10 +14,23 @@ package de.bixilon.minosoft.data.mappings.versions; import com.google.common.collect.HashBiMap; +import com.google.gson.JsonObject; +import de.bixilon.minosoft.Minosoft; +import de.bixilon.minosoft.config.ConfigurationPaths; +import de.bixilon.minosoft.data.Mappings; +import de.bixilon.minosoft.data.assets.AssetsManager; +import de.bixilon.minosoft.data.assets.Resources; +import de.bixilon.minosoft.data.locale.minecraft.MinecraftLocaleManager; import de.bixilon.minosoft.protocol.protocol.ConnectionStates; import de.bixilon.minosoft.protocol.protocol.Packets; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; +import de.bixilon.minosoft.util.CountUpAndDownLatch; +import de.bixilon.minosoft.util.Util; +import de.bixilon.minosoft.util.logging.Log; +import de.bixilon.minosoft.util.logging.LogLevels; +import javax.annotation.Nullable; +import java.io.IOException; import java.util.HashMap; public class Version { @@ -28,6 +41,8 @@ public class Version { String versionName; VersionMapping mapping; boolean isGettingLoaded; + private AssetsManager assetsManager; + private MinecraftLocaleManager localeManager; public Version(String versionName, int versionId, int protocolId, HashMap> serverboundPacketMapping, HashMap> clientboundPacketMapping) { this.versionName = versionName; @@ -122,4 +137,92 @@ public class Version { public boolean isLoaded() { return getMapping() != null && getMapping().isFullyLoaded(); } + + public AssetsManager getAssetsManager() { + return this.assetsManager; + } + + public void setAssetsManager(AssetsManager assetsManager) { + this.assetsManager = assetsManager; + } + + public MinecraftLocaleManager getLocaleManager() { + return this.localeManager; + } + + public void initializeAssetManger(CountUpAndDownLatch latch) throws Exception { + if (this.assetsManager == null) { + this.assetsManager = new AssetsManager(Minosoft.getConfig().getBoolean(ConfigurationPaths.BooleanPaths.DEBUG_VERIFY_ASSETS), Resources.getAssetVersionByVersion(this)); + this.assetsManager.downloadAllAssets(latch); + this.localeManager = new MinecraftLocaleManager(this); + } + } + + public void loadMappings(CountUpAndDownLatch latch) throws IOException { + if (isLoaded()) { + // already loaded + return; + } + Version preFlatteningVersion = Versions.getVersionById(ProtocolDefinition.PRE_FLATTENING_VERSION_ID); + if (!isFlattened() && this != preFlatteningVersion && !preFlatteningVersion.isLoaded()) { + // no matter what, we need the version mapping for all pre flattening versions + preFlatteningVersion.loadMappings(latch); + } + if (isGettingLoaded()) { + // async: we don't wanna load this version twice, skip + return; + } + latch.countUp(); + this.isGettingLoaded = true; + Log.verbose(String.format("Loading mappings for version %s...", this)); + long startTime = System.currentTimeMillis(); + + HashMap files; + try { + files = Util.readJsonTarStream(AssetsManager.readAssetAsStreamByHash(this.assetsManager.getAssetVersion().getMinosoftMappings())); + } catch (Exception e) { + // should not happen, but if this version is not flattened, we can fallback to the flatten mappings. Some things might not work... + Log.printException(e, LogLevels.VERBOSE); + if (isFlattened() || getVersionId() == ProtocolDefinition.FLATTING_VERSION_ID) { + throw e; + } + files = new HashMap<>(); + } + + latch.addCount(Mappings.VALUES.length); + for (Mappings mapping : Mappings.VALUES) { + JsonObject data = null; + if (files.containsKey(mapping.getFilename() + ".json")) { + data = files.get(mapping.getFilename() + ".json"); + } + if (data == null) { + loadVersionMappings(mapping, ProtocolDefinition.DEFAULT_MOD, null); + latch.countDown(); + continue; + } + for (String mod : data.keySet()) { + loadVersionMappings(mapping, mod, data.getAsJsonObject(mod)); + } + latch.countDown(); + } + if (!files.isEmpty()) { + Log.verbose(String.format("Loaded mappings for version %s in %dms (%s)", this, (System.currentTimeMillis() - startTime), getVersionName())); + } else { + Log.verbose(String.format("Could not load mappings for version %s. Some features will be unavailable.", this)); + } + this.isGettingLoaded = false; + latch.countDown(); + } + + public void loadVersionMappings(Mappings type, String mod, @Nullable JsonObject data) { + if (this.mapping == null) { + this.mapping = new VersionMapping(this); + } + this.mapping.load(type, mod, data, this); + + if (getVersionId() == ProtocolDefinition.PRE_FLATTENING_VERSION_ID && Versions.PRE_FLATTENING_MAPPING == null) { + Versions.PRE_FLATTENING_MAPPING = this.mapping; + } + } + } diff --git a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java index d1c03a5d1..57e49b409 100644 --- a/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java +++ b/src/main/java/de/bixilon/minosoft/data/mappings/versions/Versions.java @@ -16,26 +16,16 @@ package de.bixilon.minosoft.data.mappings.versions; import com.google.common.collect.HashBiMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import de.bixilon.minosoft.data.Mappings; -import de.bixilon.minosoft.data.assets.AssetsManager; import de.bixilon.minosoft.protocol.protocol.ConnectionStates; import de.bixilon.minosoft.protocol.protocol.Packets; -import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; -import de.bixilon.minosoft.util.Util; -import de.bixilon.minosoft.util.logging.Log; -import de.bixilon.minosoft.util.logging.LogLevels; -import javax.annotation.Nullable; -import java.io.IOException; import java.util.HashMap; -import java.util.HashSet; public class Versions { public static final Version LOWEST_VERSION_SUPPORTED = new Version("Automatic", -1, -1, null, null); private static final HashBiMap VERSION_ID_MAP = HashBiMap.create(500); private static final HashBiMap VERSION_PROTOCOL_ID_MAP = HashBiMap.create(500); private static final HashBiMap VERSION_NAME_MAP = HashBiMap.create(500); - private static final HashSet LOADED_VERSIONS = new HashSet<>(); public static VersionMapping PRE_FLATTENING_MAPPING; public static Version getVersionById(int versionId) { @@ -106,72 +96,6 @@ public class Versions { return version; } - public static void loadVersionMappings(Version version) throws IOException { - if (version.isLoaded()) { - // already loaded - return; - } - Version preFlatteningVersion = VERSION_ID_MAP.get(ProtocolDefinition.PRE_FLATTENING_VERSION_ID); - if (!version.isFlattened() && version != preFlatteningVersion && !preFlatteningVersion.isLoaded()) { - // no matter what, we need the version mapping for all pre flattening versions - loadVersionMappings(preFlatteningVersion); - } - if (version.isGettingLoaded()) { - // async: we don't wanna load this version twice, skip - return; - } - version.setGettingLoaded(true); - Log.verbose(String.format("Loading mappings for version %s...", version)); - long startTime = System.currentTimeMillis(); - - HashMap files; - try { - files = Util.readJsonTarStream(AssetsManager.readAssetAsStream(String.format("mappings/%s", version.getVersionName()))); - } catch (Exception e) { - // should not happen, but if this version is not flattened, we can fallback to the flatten mappings. Some things might not work... - Log.printException(e, LogLevels.VERBOSE); - if (version.isFlattened() || version.getVersionId() == ProtocolDefinition.FLATTING_VERSION_ID) { - throw e; - } - files = new HashMap<>(); - } - - for (Mappings mapping : Mappings.values()) { - JsonObject data = null; - if (files.containsKey(mapping.getFilename() + ".json")) { - data = files.get(mapping.getFilename() + ".json"); - } - if (data == null) { - loadVersionMappings(mapping, ProtocolDefinition.DEFAULT_MOD, null, version); - continue; - } - for (String mod : data.keySet()) { - loadVersionMappings(mapping, mod, data.getAsJsonObject(mod), version); - } - } - if (!files.isEmpty()) { - Log.verbose(String.format("Loaded mappings for version %s in %dms (%s)", version, (System.currentTimeMillis() - startTime), version.getVersionName())); - } else { - Log.verbose(String.format("Could not load mappings for version %s. Some features will be unavailable.", version)); - } - version.setGettingLoaded(false); - } - - public static void loadVersionMappings(Mappings type, String mod, @Nullable JsonObject data, Version version) { - VersionMapping mapping; - mapping = version.getMapping(); - if (mapping == null) { - mapping = new VersionMapping(version); - version.setMapping(mapping); - } - mapping.load(type, mod, data, version); - - if (version.getVersionId() == ProtocolDefinition.PRE_FLATTENING_VERSION_ID && PRE_FLATTENING_MAPPING == null) { - PRE_FLATTENING_MAPPING = mapping; - } - LOADED_VERSIONS.add(version); - } - public static HashBiMap getVersionIdMap() { return VERSION_ID_MAP; } diff --git a/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.java b/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.java index 262a1c055..fabf9915c 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.java +++ b/src/main/java/de/bixilon/minosoft/data/text/BaseComponent.java @@ -17,6 +17,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import de.bixilon.minosoft.data.mappings.versions.Version; import de.bixilon.minosoft.modding.event.events.annotations.Unsafe; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; import de.bixilon.minosoft.util.hash.BetterHashSet; @@ -35,11 +36,11 @@ public class BaseComponent extends ChatComponent { public BaseComponent() { } - public BaseComponent(String text) { - this(null, text); + public BaseComponent(Version version, String text) { + this(version, null, text); } - public BaseComponent(@Nullable ChatComponent parent, String text) { + public BaseComponent(Version version, @Nullable ChatComponent parent, String text) { // legacy String StringBuilder currentText = new StringBuilder(); RGBColor color = null; @@ -92,19 +93,19 @@ public class BaseComponent extends ChatComponent { } } - public BaseComponent(JsonObject json) { - this(null, json); + public BaseComponent(Version version, JsonObject json) { + this(version, null, json); } @SuppressWarnings("unchecked") - public BaseComponent(@Nullable TextComponent parent, JsonElement data) { + public BaseComponent(Version version, @Nullable TextComponent parent, JsonElement data) { MultiChatComponent thisTextComponent = null; if (data instanceof JsonObject json) { if (json.has("text")) { String text = json.get("text").getAsString(); if (text.contains(String.valueOf(ProtocolDefinition.TEXT_COMPONENT_SPECIAL_PREFIX_CHAR))) { // legacy text component - this.parts.add(new BaseComponent(text)); + this.parts.add(new BaseComponent(version, text)); return; } RGBColor color; @@ -160,14 +161,14 @@ public class BaseComponent extends ChatComponent { final TextComponent parentParameter = thisTextComponent == null ? parent : thisTextComponent; if (json.has("extra")) { JsonArray extras = json.getAsJsonArray("extra"); - extras.forEach((extra -> this.parts.add(new BaseComponent(parentParameter, extra)))); + extras.forEach((extra -> this.parts.add(new BaseComponent(version, parentParameter, extra)))); } if (json.has("translate")) { - this.parts.add(new TranslatableComponent(parentParameter, json.get("translate").getAsString(), json.getAsJsonArray("with"))); + this.parts.add(new TranslatableComponent(version, parentParameter, json.get("translate").getAsString(), json.getAsJsonArray("with"))); } } else if (data instanceof JsonPrimitive primitive) { - this.parts.add(new BaseComponent(parent, primitive.getAsString())); + this.parts.add(new BaseComponent(version, parent, primitive.getAsString())); } } @@ -217,8 +218,8 @@ public class BaseComponent extends ChatComponent { return this; } - public BaseComponent append(String message) { - this.parts.add(new BaseComponent(message)); + public BaseComponent append(Version version, String message) { + this.parts.add(new BaseComponent(version, message)); return this; } diff --git a/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.java b/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.java index 491649086..d6d71b52f 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.java +++ b/src/main/java/de/bixilon/minosoft/data/text/ChatComponent.java @@ -17,6 +17,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import de.bixilon.minosoft.data.mappings.versions.Version; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Node; @@ -25,10 +26,14 @@ import javax.annotation.Nullable; public abstract class ChatComponent { public static ChatComponent valueOf(Object raw) { - return valueOf(null, raw); + return valueOf(null, null, raw); } - public static ChatComponent valueOf(@Nullable TextComponent parent, Object raw) { + public static ChatComponent valueOf(Version version, Object raw) { + return valueOf(version, null, raw); + } + + public static ChatComponent valueOf(Version version, @Nullable TextComponent parent, Object raw) { if (raw == null) { return new BaseComponent(); } @@ -43,13 +48,13 @@ public abstract class ChatComponent { try { json = JsonParser.parseString((String) raw).getAsJsonObject(); } catch (JsonParseException | IllegalStateException ignored) { - return new BaseComponent((String) raw); + return new BaseComponent(version, (String) raw); } } else { - return new BaseComponent(parent, raw.toString()); + return new BaseComponent(version, parent, raw.toString()); // throw new IllegalArgumentException(String.format("%s is not a valid type here!", raw.getClass().getSimpleName())); } - return new BaseComponent(parent, json); + return new BaseComponent(version, parent, json); } /** diff --git a/src/main/java/de/bixilon/minosoft/data/text/TranslatableComponent.java b/src/main/java/de/bixilon/minosoft/data/text/TranslatableComponent.java index d303ac382..ada6b7b7f 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/TranslatableComponent.java +++ b/src/main/java/de/bixilon/minosoft/data/text/TranslatableComponent.java @@ -14,7 +14,7 @@ package de.bixilon.minosoft.data.text; import com.google.gson.JsonArray; -import de.bixilon.minosoft.data.locale.minecraft.MinecraftLocaleManager; +import de.bixilon.minosoft.data.mappings.versions.Version; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; import javafx.collections.ObservableList; import javafx.scene.Node; @@ -27,12 +27,14 @@ public class TranslatableComponent extends ChatComponent { private final ArrayList data = new ArrayList<>(); private final String key; private final TextComponent parent; + private final Version version; - public TranslatableComponent(String key, JsonArray data) { - this(null, key, data); + public TranslatableComponent(Version version, String key, JsonArray data) { + this(version, null, key, data); } - public TranslatableComponent(@Nullable TextComponent parent, String key, JsonArray data) { + public TranslatableComponent(Version version, @Nullable TextComponent parent, String key, JsonArray data) { + this.version = version; this.parent = parent; this.key = key; if (data == null) { @@ -40,9 +42,9 @@ public class TranslatableComponent extends ChatComponent { } data.forEach((jsonElement -> { if (jsonElement.isJsonPrimitive()) { - this.data.add(ChatComponent.valueOf(parent, jsonElement.getAsString())); + this.data.add(ChatComponent.valueOf(version, parent, jsonElement.getAsString())); } else { - this.data.add(new BaseComponent(parent, jsonElement.getAsJsonObject())); + this.data.add(new BaseComponent(version, parent, jsonElement.getAsJsonObject())); } })); } @@ -67,7 +69,7 @@ public class TranslatableComponent extends ChatComponent { // ToDo fix nested base component (formatting), not just a string // This is just a dirty workaround to enable formatting and coloring. Still need to do hover, click, ... stuff - return new BaseComponent(getLegacyText()).getJavaFXText(nodes); + return new BaseComponent(this.version, getLegacyText()).getJavaFXText(nodes); } // just used reflections to not write this twice anc only change the method name @@ -94,7 +96,7 @@ public class TranslatableComponent extends ChatComponent { }); } } - builder.append(MinecraftLocaleManager.translate(this.key, data)); + builder.append(this.version.getLocaleManager().translate(this.key, data)); for (ChatFormattingCode code : this.parent.getFormatting()) { if (code instanceof PostChatFormattingCodes postCode) { builder.append(switch (methodName) { @@ -106,7 +108,7 @@ public class TranslatableComponent extends ChatComponent { } return builder.toString(); } - String text = MinecraftLocaleManager.translate(this.key, data); + String text = this.version.getLocaleManager().translate(this.key, data); if (text == null) { // Error, can not translate text = "{invalid=true, key=" + this.key + ", data=" + Arrays.toString(data); diff --git a/src/main/java/de/bixilon/minosoft/generator/JarHashGenerator.java b/src/main/java/de/bixilon/minosoft/generator/JarHashGenerator.java new file mode 100644 index 000000000..711b47d08 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/generator/JarHashGenerator.java @@ -0,0 +1,46 @@ +package de.bixilon.minosoft.generator; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import de.bixilon.minosoft.data.assets.AssetsManager; +import de.bixilon.minosoft.data.assets.Resources; +import de.bixilon.minosoft.data.mappings.versions.Version; + +import java.io.*; + +public class JarHashGenerator { + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Usage: JarHashGenerator "); + return; + } + try { + Version version = new Version(args[0], -1, -1, null, null); + + JsonObject json = JsonParser.parseReader(new InputStreamReader(new FileInputStream("src/main/resources/assets/mapping/resources.json"))).getAsJsonObject(); + + + JsonObject versions = json.getAsJsonObject("versions"); + + JsonObject versionJson = versions.getAsJsonObject(version.getVersionName()); + + Resources.loadVersion(version, versionJson); + + AssetsManager assetsManager = new AssetsManager(true, Resources.getAssetVersionByVersion(version)); + String jarAssetsHash = assetsManager.generateJarAssets(); + + versionJson.addProperty("jar_assets_hash", jarAssetsHash); + + + File file = new File("src/main/resources/assets/mapping/resources.json"); + FileWriter writer = new FileWriter(file.getAbsoluteFile()); + writer.write(new Gson().toJson(json)); + writer.close(); + System.exit(0); + } catch (Exception e) { + System.exit(1); + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/gui/main/MainWindow.java b/src/main/java/de/bixilon/minosoft/gui/main/MainWindow.java index 057d99671..a51e9dc24 100644 --- a/src/main/java/de/bixilon/minosoft/gui/main/MainWindow.java +++ b/src/main/java/de/bixilon/minosoft/gui/main/MainWindow.java @@ -24,7 +24,7 @@ import de.bixilon.minosoft.data.accounts.Account; import de.bixilon.minosoft.data.locale.LocaleManager; import de.bixilon.minosoft.data.locale.Strings; import de.bixilon.minosoft.data.mappings.versions.Versions; -import de.bixilon.minosoft.data.text.BaseComponent; +import de.bixilon.minosoft.data.text.ChatComponent; import de.bixilon.minosoft.protocol.protocol.LANServerListener; import de.bixilon.minosoft.util.DNSUtil; import de.bixilon.minosoft.util.logging.Log; @@ -180,7 +180,7 @@ public class MainWindow implements Initializable { submitButton.setOnAction(actionEvent -> { Server server1 = server; - BaseComponent serverName = new BaseComponent(serverNameField.getText()); + ChatComponent serverName = ChatComponent.valueOf(serverNameField.getText()); String serverAddress = DNSUtil.correctHostName(serverAddressField.getText()); int desiredVersionId = GUITools.VERSION_COMBO_BOX.getSelectionModel().getSelectedItem().getVersionId(); diff --git a/src/main/java/de/bixilon/minosoft/gui/main/Server.java b/src/main/java/de/bixilon/minosoft/gui/main/Server.java index ea218b7ad..d270dcf70 100644 --- a/src/main/java/de/bixilon/minosoft/gui/main/Server.java +++ b/src/main/java/de/bixilon/minosoft/gui/main/Server.java @@ -52,7 +52,7 @@ public class Server { } this.name = name; this.address = address; - this.addressName = new BaseComponent(address); + this.addressName = ChatComponent.valueOf(address); this.desiredVersion = desiredVersion; } @@ -62,7 +62,7 @@ public class Server { public Server(ServerAddress address) { this.id = getNextServerId(); - this.name = new BaseComponent(String.format("LAN Server #%d", LANServerListener.getServerMap().size())); + this.name = ChatComponent.valueOf(String.format("LAN Server #%d", LANServerListener.getServerMap().size())); this.address = address.toString(); this.desiredVersion = -1; // Automatic this.readOnly = true; @@ -130,7 +130,7 @@ public class Server { return this.name; } - public void setName(BaseComponent name) { + public void setName(ChatComponent name) { this.name = name; } @@ -140,7 +140,7 @@ public class Server { public void setAddress(String address) { this.address = address; - this.addressName = new BaseComponent(address); + this.addressName = ChatComponent.valueOf(address); } public void ping() { diff --git a/src/main/java/de/bixilon/minosoft/gui/main/ServerListCell.java b/src/main/java/de/bixilon/minosoft/gui/main/ServerListCell.java index 40c7d370e..bbdda38fc 100644 --- a/src/main/java/de/bixilon/minosoft/gui/main/ServerListCell.java +++ b/src/main/java/de/bixilon/minosoft/gui/main/ServerListCell.java @@ -32,6 +32,7 @@ import de.bixilon.minosoft.protocol.network.Connection; import de.bixilon.minosoft.protocol.ping.ForgeModInfo; import de.bixilon.minosoft.protocol.ping.ServerListPing; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; +import de.bixilon.minosoft.util.CountUpAndDownLatch; import de.bixilon.minosoft.util.logging.Log; import javafx.application.Platform; import javafx.fxml.FXMLLoader; @@ -284,17 +285,21 @@ public class ServerListCell extends ListCell implements Initializable { if (!this.canConnect || this.server.getLastPing() == null) { return; } - Connection connection = new Connection(Connection.lastConnectionId++, this.server.getAddress(), new Player(Minosoft.getConfig().getSelectedAccount())); - Version version; - if (this.server.getDesiredVersionId() == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) { - version = this.server.getLastPing().getVersion(); - } else { - version = Versions.getVersionById(this.server.getDesiredVersionId()); - } - this.optionsConnect.setDisable(true); - connection.connect(this.server.getLastPing().getAddress(), version); - connection.registerEvent(new EventInvokerCallback<>(ConnectionStateChangeEvent.class, this::handleConnectionCallback)); - this.server.addConnection(connection); + new Thread(() -> { + Connection connection = new Connection(Connection.lastConnectionId++, this.server.getAddress(), new Player(Minosoft.getConfig().getSelectedAccount())); + Version version; + if (this.server.getDesiredVersionId() == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) { + version = this.server.getLastPing().getVersion(); + } else { + version = Versions.getVersionById(this.server.getDesiredVersionId()); + } + this.optionsConnect.setDisable(true); + // ToDo: show progress dialog + + connection.connect(this.server.getLastPing().getAddress(), version, new CountUpAndDownLatch(1)); + connection.registerEvent(new EventInvokerCallback<>(ConnectionStateChangeEvent.class, this::handleConnectionCallback)); + this.server.addConnection(connection); + }, "ConnectThread").start(); } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java index 4fbf244ff..2e6edafe7 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java +++ b/src/main/java/de/bixilon/minosoft/protocol/network/Connection.java @@ -33,6 +33,7 @@ import de.bixilon.minosoft.protocol.ping.ServerListPing; import de.bixilon.minosoft.protocol.protocol.*; import de.bixilon.minosoft.terminal.CLI; import de.bixilon.minosoft.terminal.commands.commands.Command; +import de.bixilon.minosoft.util.CountUpAndDownLatch; import de.bixilon.minosoft.util.DNSUtil; import de.bixilon.minosoft.util.ServerAddress; import de.bixilon.minosoft.util.logging.Log; @@ -123,10 +124,21 @@ public class Connection { this.network.connect(this.address); } - public void connect(ServerAddress address, Version version) { + public void connect(ServerAddress address, Version version, CountUpAndDownLatch latch) { this.address = address; this.reason = ConnectionReasons.CONNECT; setVersion(version); + try { + version.initializeAssetManger(latch); // ToDo + version.loadMappings(latch); + this.customMapping.setVersion(version); + this.customMapping.setParentMapping(version.getMapping()); + } catch (Exception e) { + Log.printException(e, LogLevels.DEBUG); + Log.fatal(String.format("Could not load mapping for %s. This version seems to be unsupported!", version)); + this.lastException = new MappingsLoadingException("Mappings could not be loaded", e); + setConnectionState(ConnectionStates.FAILED_NO_RETRY); + } Log.info(String.format("Connecting to server: %s", address)); this.network.connect(address); } @@ -149,18 +161,9 @@ public class Connection { } this.version = version; - try { - Versions.loadVersionMappings(version); - this.customMapping.setVersion(version); - this.customMapping.setParentMapping(version.getMapping()); - } catch (Exception e) { - Log.printException(e, LogLevels.DEBUG); - Log.fatal(String.format("Could not load mapping for %s. This version seems to be unsupported!", version)); - this.lastException = new MappingsLoadingException("Mappings could not be loaded", e); - setConnectionState(ConnectionStates.FAILED_NO_RETRY); - } } + public void handle(ClientboundPacket p) { this.handlingQueue.add(p); } diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/login/PacketEncryptionRequest.java b/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/login/PacketEncryptionRequest.java index 4e44b772e..75f4eb78c 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/login/PacketEncryptionRequest.java +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/login/PacketEncryptionRequest.java @@ -27,7 +27,6 @@ import java.math.BigInteger; import java.security.PublicKey; public class PacketEncryptionRequest extends ClientboundPacket { - String serverId; // normally empty byte[] publicKey; byte[] verifyToken; diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/status/PacketStatusResponse.java b/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/status/PacketStatusResponse.java index 419a6b498..fcf5842d5 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/status/PacketStatusResponse.java +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/clientbound/status/PacketStatusResponse.java @@ -30,7 +30,7 @@ public class PacketStatusResponse extends ClientboundPacket { @Override public boolean read(InByteBuffer buffer) { - this.response = new ServerListPing(buffer.readJSON()); + this.response = new ServerListPing(buffer.getConnection().getVersion(), buffer.readJSON()); return true; } diff --git a/src/main/java/de/bixilon/minosoft/protocol/ping/ServerListPing.java b/src/main/java/de/bixilon/minosoft/protocol/ping/ServerListPing.java index 312fd3db5..904b68751 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/ping/ServerListPing.java +++ b/src/main/java/de/bixilon/minosoft/protocol/ping/ServerListPing.java @@ -14,6 +14,7 @@ package de.bixilon.minosoft.protocol.ping; import com.google.gson.JsonObject; +import de.bixilon.minosoft.data.mappings.versions.Version; import de.bixilon.minosoft.data.text.BaseComponent; import de.bixilon.minosoft.data.text.ChatComponent; import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition; @@ -30,7 +31,7 @@ public class ServerListPing { private final String serverBrand; byte[] favicon; - public ServerListPing(JsonObject json) { + public ServerListPing(Version version, JsonObject json) { int protocolId = json.getAsJsonObject("version").get("protocol").getAsInt(); if (protocolId == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) { // Server did not send us a version, trying 1.8 @@ -48,7 +49,7 @@ public class ServerListPing { if (json.get("description").isJsonPrimitive()) { this.motd = ChatComponent.valueOf(json.get("description").getAsString()); } else { - this.motd = new BaseComponent(json.getAsJsonObject("description")); + this.motd = new BaseComponent(version, json.getAsJsonObject("description")); } this.serverBrand = json.getAsJsonObject("version").get("name").getAsString(); diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.java index 1aaa6e59d..d93742e19 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/InByteBuffer.java @@ -202,7 +202,7 @@ public class InByteBuffer { } public ChatComponent readChatComponent() { - return ChatComponent.valueOf(readString()); + return ChatComponent.valueOf(this.connection.getVersion(), readString()); } @IntRange(from = 0) @@ -227,7 +227,7 @@ public class InByteBuffer { if (this.versionId < V_17W45A) { // old particle format return switch (type.getFullIdentifier()) { - case "minecraft:iconcrack" -> new ItemParticleData(new Slot(this.connection.getMapping().getItemByLegacy(readVarInt(), readVarInt())), type); + case "minecraft:iconcrack" -> new ItemParticleData(new Slot(this.connection.getVersion(), this.connection.getMapping().getItemByLegacy(readVarInt(), readVarInt())), type); case "minecraft:blockcrack", "minecraft:blockdust", "minecraft:falling_dust" -> new BlockParticleData(this.connection.getMapping().getBlockById(readVarInt() << 4), type); default -> new ParticleData(type); }; @@ -298,10 +298,10 @@ public class InByteBuffer { metaData = readShort(); } CompoundTag nbt = (CompoundTag) readNBT(this.versionId < V_14W28B); - return new Slot(this.connection.getMapping(), this.connection.getMapping().getItemByLegacy(id, metaData), count, metaData, nbt); + return new Slot(this.connection.getVersion(), this.connection.getMapping().getItemByLegacy(id, metaData), count, metaData, nbt); } if (readBoolean()) { - return new Slot(this.connection.getMapping(), this.connection.getMapping().getItemById(readVarInt()), readByte(), (CompoundTag) readNBT()); + return new Slot(this.connection.getVersion(), this.connection.getMapping().getItemById(readVarInt()), readByte(), (CompoundTag) readNBT()); } return null; } diff --git a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java index 0f1503c41..1e86e7671 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java +++ b/src/main/java/de/bixilon/minosoft/protocol/protocol/ProtocolDefinition.java @@ -62,8 +62,9 @@ public final class ProtocolDefinition { public static final String MOJANG_URL_VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; - public static final String MOJANG_URL_RESOURCES = "https://resources.download.minecraft.net/%s/%s"; - public static final String MOJANG_URL_PACKAGES = "https://launchermeta.mojang.com/v1/packages/"; + public static final String MINECRAFT_URL_RESOURCES = "https://resources.download.minecraft.net/%s/%s"; + public static final String MOJANG_URL_PACKAGES = "https://launchermeta.mojang.com/v1/packages/%s/%s"; + public static final String MOJANG_LAUNCHER_URL_PACKAGES = "https://launcher.mojang.com/v1/objects/%s/%s"; public static final String MOJANG_URL_BLOCKED_SERVERS = "https://sessionserver.mojang.com/blockedservers"; public static final String MOJANG_URL_LOGIN = "https://authserver.mojang.com/authenticate"; diff --git a/util/block_model_generator.py b/util/block_model_generator.py new file mode 100644 index 000000000..a3353ab1e --- /dev/null +++ b/util/block_model_generator.py @@ -0,0 +1,141 @@ +# Minosoft +# Copyright (C) 2020 Lukas Eisenhauer +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program.If not, see . +# +# This software is not affiliated with Mojang AB, the original developer of Minecraft. + +import io +import sys +import urllib.request +import zipfile + +import ujson + +if len(sys.argv) != 3: + print("Usage: %s " % sys.argv[0]) + exit(1) + +blockStates = {} +blockModels = {} + +modName = "minecraft" + +print("Downloading minecraft jar...") + +# thanks: https://stackoverflow.com/questions/60171502/requests-get-is-very-slow + +response = urllib.request.urlopen(sys.argv[2]) +print("Unpacking minecraft jar...") + +zip = zipfile.ZipFile(io.BytesIO(response.read()), "r") + +files = zip.namelist() + +print("Loading blockstates...") + + +def readRotations(apply, current): + if "x" in current: + apply["x"] = current["x"] + if "y" in current: + apply["y"] = current["y"] + if "z" in current: + apply["z"] = current["z"] + + +def readPart(part): + properties = [] + if "when" in part: + when = part["when"] + if "OR" in when: + for item in when["OR"]: + properties.append(item) + else: + properties.append(part["when"]) + apply = {} + current = part["apply"] + if type(current) == type([]): + current = current[0] + if "/" in current["model"]: + apply["model"] = current["model"].split("/")[1] + else: + apply["model"] = current["model"] + readRotations(apply, current) + result = [] + for item in properties: + state = {"properties": item} + for i in apply: + state[i] = apply[i] + result.append(state) + if len(result) == 0: + result.append(apply) + return result + + +for blockStateFile in [f for f in files if f.startswith('assets/minecraft/blockstates/')]: + with zip.open(blockStateFile) as file: + tempData = file.read().decode("utf-8") + if tempData.endswith("n"): + # why the hell are mojangs json files incorrect? + # in 19w02a (https://launcher.mojang.com/v1/objects/8664f5d1b428d5ba8a936ab9c097cc78821d06e6/client.jar) the json ends with a random "n" + tempData = tempData[:-1] + data = ujson.loads(tempData) + block = {} + if "variants" in data: + variants = data["variants"] + states = [] + for variant in variants: + state = {} + properties = {} + if variant != "" and variant != "normal" and variant != "map" and variant != "all": + for part in variant.split(","): + properties[part.split("=")[0]] = part.split("=")[1] + state["properties"] = properties + current = variants[variant] + if type(current) == type([]): + current = current[0] + + if "/" in current["model"]: + state["model"] = current["model"].split("/")[1] + else: + state["model"] = current["model"] + readRotations(state, current) + states.append(state) + block = { + "states": states + } + elif "multipart" in data: + parts = data["multipart"] + conditional = [] + for part in parts: + conditional.extend(readPart(part)) + block = { + "conditional": conditional + } + blockStates[blockStateFile.split(".")[0].split("/")[-1]] = block + +print("Loading models...") +for blockModelFile in [f for f in files if f.startswith('assets/minecraft/models/block/')]: + with zip.open(blockModelFile) as file: + data = ujson.load(file) + blockModels[blockModelFile.split(".")[0].split("/")[-1]] = data + +print("Combining files...") +finalJson = { + modName: { + "blockStates": blockStates, + "blockModels": blockModels + } +} + +print("Saving...") +with open(sys.argv[1], "w+") as file: + finalJson = ujson.dumps(finalJson) + file.write(finalJson.replace("minecraft:", "")) + +print("Finished successfully") diff --git a/util/version_mappings_generator.py b/util/version_mappings_generator.py index 3ef0c75a5..837b34837 100644 --- a/util/version_mappings_generator.py +++ b/util/version_mappings_generator.py @@ -12,10 +12,12 @@ import hashlib import os import re -import requests import shutil +import subprocess import tarfile import traceback + +import requests import ujson print("Minecraft mappings downloader (and generator)") @@ -24,7 +26,7 @@ PRE_FLATTENING_UPDATE_VERSION = "17w46a" DATA_FOLDER = "../data/resources/" TEMP_FOLDER = DATA_FOLDER + "tmp/" OPTIONAL_FILES_PER_VERSION = ["entities.json"] -FILES_PER_VERSION = ["blocks.json", "registries.json"] + OPTIONAL_FILES_PER_VERSION +FILES_PER_VERSION = ["blocks.json", "registries.json", "block_models.json"] + OPTIONAL_FILES_PER_VERSION DOWNLOAD_BASE_URL = "https://apimon.de/mcdata/" RESOURCE_MAPPINGS_INDEX = ujson.load(open("../src/main/resources/assets/mapping/resources.json")) MOJANG_MINOSOFT_FIELD_MAPPINGS = ujson.load(open("entitiesFieldMojangMinosoftMappings.json")) @@ -128,14 +130,50 @@ if not os.path.isdir(DATA_FOLDER): if not os.path.isdir(TEMP_FOLDER): os.mkdir(TEMP_FOLDER) + +# compile minosoft + + +def generateJarAssets(versionId): + generateProcess = "" + try: + generateProcess = subprocess.run(r'mvn exec:java -Dexec.mainClass="de.bixilon.minosoft.generator.JarHashGenerator" -Dexec.args="%s"' % versionId, cwd=r'../', shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # reload mappings + global RESOURCE_MAPPINGS_INDEX + RESOURCE_MAPPINGS_INDEX = ujson.load(open("../src/main/resources/assets/mapping/resources.json")) + except Exception: + print(generateProcess.stdout) + print(generateProcess.stderr) + + +print("Compiling minosoft...") +compileProcess = "" +try: + compileProcess = subprocess.run(r'mvn compile', shell=True, check=True, cwd=r'../', stdout=subprocess.PIPE, stderr=subprocess.PIPE) +except Exception: + print(compileProcess.stdout) + print(compileProcess.stderr) + exit(1) +print("Minosoft compiled!") + for version in VERSION_MANIFEST["versions"]: if version["id"] == PRE_FLATTENING_UPDATE_VERSION: break versionTempBaseFolder = TEMP_FOLDER + version["id"] + "/" - resourcesJsonKey = ("mappings/%s" % version["id"]) - if resourcesJsonKey in RESOURCE_MAPPINGS_INDEX and os.path.isfile(DATA_FOLDER + RESOURCE_MAPPINGS_INDEX[resourcesJsonKey][:2] + "/" + RESOURCE_MAPPINGS_INDEX[resourcesJsonKey] + ".tar.gz"): - print("Skipping %s" % (version["id"])) - continue + resourcesVersion = {} + if version["id"] in RESOURCE_MAPPINGS_INDEX["versions"]: + resourcesVersion = RESOURCE_MAPPINGS_INDEX["versions"][version["id"]] + if os.path.isfile(DATA_FOLDER + resourcesVersion["mappings"][:2] + "/" + resourcesVersion["mappings"] + ".tar.gz"): + if "jar_assets_hash" not in resourcesVersion: + print("=== %s === " % version["id"]) + try: + generateJarAssets(version["id"]) + except Exception: + failedVersionIds.append(version["id"]) + continue + else: + print("Skipping %s" % (version["id"])) + continue print() print("=== %s === " % version["id"]) @@ -306,6 +344,10 @@ for version in VERSION_MANIFEST["versions"]: with open(versionTempBaseFolder + "entities.json", 'w') as file: file.write(ujson.dumps({"minecraft": entities})) + elif fileName == "block_models.json": + # blockModelsCombiner.py will do the trick for us + os.popen('python3 block_model_generator.py \"%s\" %s' % (versionTempBaseFolder + "block_models.json", versionJson['downloads']['client']['url'])).read() + except Exception: traceback.print_exc() print("ERR: Could not generate %s for %s" % (fileName, version["id"])) @@ -337,20 +379,32 @@ for version in VERSION_MANIFEST["versions"]: os.mkdir(DATA_FOLDER + sha1[:2]) os.rename(versionTempBaseFolder + version["id"] + ".tar.gz", DATA_FOLDER + sha1[:2] + "/" + sha1 + ".tar.gz") - if resourcesJsonKey in RESOURCE_MAPPINGS_INDEX: + if "mappings" in resourcesVersion: # this file already has a mapping, delete it - hashToDelete = RESOURCE_MAPPINGS_INDEX[resourcesJsonKey] + hashToDelete = resourcesVersion["mappings"] filenameToDelete = DATA_FOLDER + hashToDelete[:2] + "/" + hashToDelete + ".tar.gz" if os.path.isfile(filenameToDelete): shutil.rmtree(filenameToDelete) - RESOURCE_MAPPINGS_INDEX[resourcesJsonKey] = sha1 + resourcesVersion["mappings"] = sha1 + resourcesVersion["index_version"] = versionJson["assetIndex"]["id"] + resourcesVersion["index_hash"] = versionJson["assetIndex"]["sha1"] + resourcesVersion["client_jar_hash"] = versionJson["downloads"]["client"]["sha1"] + + RESOURCE_MAPPINGS_INDEX["versions"][version["id"]] = resourcesVersion # cleanup (delete temp folder) shutil.rmtree(versionTempBaseFolder) # dump resources index with open("../src/main/resources/assets/mapping/resources.json", 'w') as file: ujson.dump(RESOURCE_MAPPINGS_INDEX, file) + # start jar hash generator + # todo: don't download jar twice + try: + generateJarAssets(version["id"]) + except Exception: + failedVersionIds.append(version["id"]) + print() print() print("Done")