Handle locale stuff downloading of main thread and add doc to Object2IntBiMap

Technically LocaleUtils is not thread safe and people running Geyser for the first time on slow internet connections may see some untranslated messages. However, we were seeing startup times of about 1+ minutes on these slow connections, and it's better that players see untranslated messages for a short period of time rather than having to wait over a minute for the program to start up.

Once the locale is installed, it doesn't need to be redownloaded again (unless there is a game update) and all translated messages will just work once this download is complete without clients needing to relog.
This commit is contained in:
Redned 2021-07-18 15:20:40 -05:00 committed by RednedEpic
parent b4921132e1
commit 1ad952b581
2 changed files with 129 additions and 117 deletions

View file

@ -39,6 +39,7 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
public class LocaleUtils { public class LocaleUtils {
@ -56,53 +57,55 @@ public class LocaleUtils {
localesFolder.mkdir(); localesFolder.mkdir();
// Download the latest asset list and cache it // Download the latest asset list and cache it
generateAssetCache(); generateAssetCache().whenComplete((aVoid, ex) -> downloadAndLoadLocale(LanguageUtils.getDefaultLocale()));
downloadAndLoadLocale(LanguageUtils.getDefaultLocale());
} }
/** /**
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later * Fetch the latest versions asset cache from Mojang so we can grab the locale files later
*/ */
private static void generateAssetCache() { private static CompletableFuture<Void> generateAssetCache() {
try { return CompletableFuture.supplyAsync(() -> {
// Get the version manifest from Mojang try {
VersionManifest versionManifest = GeyserConnector.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class); // Get the version manifest from Mojang
VersionManifest versionManifest = GeyserConnector.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
// Get the url for the latest version of the games manifest // Get the url for the latest version of the games manifest
String latestInfoURL = ""; String latestInfoURL = "";
for (Version version : versionManifest.getVersions()) { for (Version version : versionManifest.getVersions()) {
if (version.getId().equals(MinecraftConstants.GAME_VERSION)) { if (version.getId().equals(MinecraftConstants.GAME_VERSION)) {
latestInfoURL = version.getUrl(); latestInfoURL = version.getUrl();
break; break;
}
} }
// Make sure we definitely got a version
if (latestInfoURL.isEmpty()) {
throw new Exception(LanguageUtils.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserConnector.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
clientJarInfo = versionInfo.getDownloads().get("client");
GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(clientJarInfo));
// Get the assets list
JsonNode assets = GeyserConnector.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
Asset asset = GeyserConnector.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
} }
return null;
// Make sure we definitely got a version });
if (latestInfoURL.isEmpty()) {
throw new Exception(LanguageUtils.getLocaleStringLog("geyser.locale.fail.latest_version"));
}
// Get the individual version manifest
VersionInfo versionInfo = GeyserConnector.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
// Get the client jar for use when downloading the en_us locale
GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
clientJarInfo = versionInfo.getDownloads().get("client");
GeyserConnector.getInstance().getLogger().debug(GeyserConnector.JSON_MAPPER.writeValueAsString(clientJarInfo));
// Get the assets list
JsonNode assets = GeyserConnector.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
// Put each asset into an array for use later
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
while (assetIterator.hasNext()) {
Map.Entry<String, JsonNode> entry = assetIterator.next();
Asset asset = GeyserConnector.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
ASSET_MAP.put(entry.getKey(), asset);
}
} catch (Exception e) {
GeyserConnector.getInstance().getLogger().error(LanguageUtils.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
}
} }
/** /**
@ -311,107 +314,107 @@ public class LocaleUtils {
public static void init() { public static void init() {
// no-op // no-op
} }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class VersionManifest {
class VersionManifest { @JsonProperty("latest")
@JsonProperty("latest") private LatestVersion latestVersion;
private LatestVersion latestVersion;
@JsonProperty("versions")
@JsonProperty("versions") private List<Version> versions;
private List<Version> versions; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class LatestVersion {
class LatestVersion { @JsonProperty("release")
@JsonProperty("release") private String release;
private String release;
@JsonProperty("snapshot")
@JsonProperty("snapshot") private String snapshot;
private String snapshot; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class Version {
class Version { @JsonProperty("id")
@JsonProperty("id") private String id;
private String id;
@JsonProperty("type")
@JsonProperty("type") private String type;
private String type;
@JsonProperty("url")
@JsonProperty("url") private String url;
private String url;
@JsonProperty("time")
@JsonProperty("time") private String time;
private String time;
@JsonProperty("releaseTime")
@JsonProperty("releaseTime") private String releaseTime;
private String releaseTime; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class VersionInfo {
class VersionInfo { @JsonProperty("id")
@JsonProperty("id") private String id;
private String id;
@JsonProperty("type")
@JsonProperty("type") private String type;
private String type;
@JsonProperty("time")
@JsonProperty("time") private String time;
private String time;
@JsonProperty("releaseTime")
@JsonProperty("releaseTime") private String releaseTime;
private String releaseTime;
@JsonProperty("assetIndex")
@JsonProperty("assetIndex") private AssetIndex assetIndex;
private AssetIndex assetIndex;
@JsonProperty("downloads")
@JsonProperty("downloads") private Map<String, VersionDownload> downloads;
private Map<String, VersionDownload> downloads; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class VersionDownload {
class VersionDownload { @JsonProperty("sha1")
@JsonProperty("sha1") private String sha1;
private String sha1;
@JsonProperty("size")
@JsonProperty("size") private int size;
private int size;
@JsonProperty("url")
@JsonProperty("url") private String url;
private String url; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class AssetIndex {
class AssetIndex { @JsonProperty("id")
@JsonProperty("id") private String id;
private String id;
@JsonProperty("sha1")
@JsonProperty("sha1") private String sha1;
private String sha1;
@JsonProperty("size")
@JsonProperty("size") private int size;
private int size;
@JsonProperty("totalSize")
@JsonProperty("totalSize") private int totalSize;
private int totalSize;
@JsonProperty("url")
@JsonProperty("url") private String url;
private String url; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonIgnoreProperties(ignoreUnknown = true) @Getter
@Getter static class Asset {
class Asset { @JsonProperty("hash")
@JsonProperty("hash") private String hash;
private String hash;
@JsonProperty("size")
@JsonProperty("size") private int size;
private int size; }
} }

View file

@ -37,6 +37,15 @@ import org.jetbrains.annotations.NotNull;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
/**
* A primitive int BiMap implementation built around fastutil to
* reduce boxing and the memory footprint. Protocol has a
* {@link com.nukkitx.protocol.util.Int2ObjectBiMap} class, but it
* does not extend the Map interface making it difficult to utilize
* it in for loops and the registry system.
*
* @param <T> the value
*/
public class Object2IntBiMap<T> implements Object2IntMap<T> { public class Object2IntBiMap<T> implements Object2IntMap<T> {
private final Object2IntMap<T> forwards; private final Object2IntMap<T> forwards;
private final Int2ObjectMap<T> backwards; private final Int2ObjectMap<T> backwards;