minecraft assets per version

This commit is contained in:
Bixilon 2021-01-06 17:35:14 +01:00
parent 14904e969f
commit 1924e8d69b
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
37 changed files with 736 additions and 390 deletions

View File

@ -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.

View File

@ -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

View File

@ -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));

View File

@ -18,6 +18,7 @@ public enum Mappings {
BLOCKS,
ENTITIES;
public static final Mappings[] VALUES = values();
public String getFilename() {
return name().toLowerCase();
}

View File

@ -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?
)

View File

@ -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<String, String> assetsMap = new HashMap<>();
private static final HashMap<String, String> 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<String, String> parseAssetsIndex(String hash) throws IOException {
return parseAssetsIndex(readJsonAssetByHash(hash).getAsJsonObject());
}
private static HashMap<String, String> parseAssetsIndex(JsonObject json) {
if (json.has("objects")) {
json = json.getAsJsonObject("objects");
}
HashMap<String, String> 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<String, String> verifyAssets(AssetsSource source, CountUpAndDownLatch latch, HashMap<String, String> 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<String, String> 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<String, String> 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<String, String> parseAssetsIndex(String hash) throws IOException {
return parseAssetsIndex(readJsonAssetByHash(hash).getAsJsonObject());
}
private HashMap<String, String> 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<String, String> 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<String, String> verifyAssets(AssetsSource source, CountUpAndDownLatch latch, HashMap<String, String> 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<String, String> 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<String, String> 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;
}
}

View File

@ -15,5 +15,6 @@ package de.bixilon.minosoft.data.assets;
public enum AssetsSource {
MOJANG,
MINECRAFT,
MINOSOFT_GIT
}

View File

@ -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<Version, AssetVersion> 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<String, JsonElement> 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);
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -28,7 +28,7 @@ public class CampfireBlockEntityMetaData extends BlockEntityMetaData {
public CampfireBlockEntityMetaData(ListTag nbt) {
this.items = new Slot[4];
for (CompoundTag tag : nbt.<CompoundTag>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
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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<Enchantment, Integer> enchantments = new HashMap<>();
private final ArrayList<ChatComponent> 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").<StringTag>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").<CompoundTag>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();

View File

@ -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);
}
}

View File

@ -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<ConnectionStates, HashBiMap<Packets.Serverbound, Integer>> serverboundPacketMapping, HashMap<ConnectionStates, HashBiMap<Packets.Clientbound, Integer>> 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<String, JsonObject> 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;
}
}
}

View File

@ -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<Integer, Version> VERSION_ID_MAP = HashBiMap.create(500);
private static final HashBiMap<Integer, Version> VERSION_PROTOCOL_ID_MAP = HashBiMap.create(500);
private static final HashBiMap<String, Version> VERSION_NAME_MAP = HashBiMap.create(500);
private static final HashSet<Version> 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<String, JsonObject> 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<Integer, Version> getVersionIdMap() {
return VERSION_ID_MAP;
}

View File

@ -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;
}

View File

@ -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);
}
/**

View File

@ -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<ChatComponent> 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);

View File

@ -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 <Version>");
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);
}
}
}

View File

@ -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();

View File

@ -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() {

View File

@ -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<Server> 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();
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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";

View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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 <destination path> <jar url>" % 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")

View File

@ -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")