mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Support new default Java skins from 1.19.3
This commit is contained in:
parent
ad4424d2b6
commit
09f17f59ae
14 changed files with 704 additions and 480 deletions
|
@ -30,7 +30,6 @@ import java.net.URISyntaxException;
|
|||
|
||||
public final class Constants {
|
||||
public static final URI GLOBAL_API_WS_URI;
|
||||
public static final String NTP_SERVER = "time.cloudflare.com";
|
||||
|
||||
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
|
||||
public static final String NEWS_PROJECT_NAME = "geyser";
|
||||
|
|
|
@ -79,6 +79,7 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
|
||||
import org.geysermc.geyser.session.SessionManager;
|
||||
import org.geysermc.geyser.skin.FloodgateSkinUploader;
|
||||
import org.geysermc.geyser.skin.ProvidedSkins;
|
||||
import org.geysermc.geyser.skin.SkinProvider;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import org.geysermc.geyser.text.MinecraftLocale;
|
||||
|
@ -95,6 +96,7 @@ import java.net.UnknownHostException;
|
|||
import java.security.Key;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
@ -195,7 +197,23 @@ public class GeyserImpl implements GeyserApi {
|
|||
EntityDefinitions.init();
|
||||
ItemTranslator.init();
|
||||
MessageTranslator.init();
|
||||
MinecraftLocale.init();
|
||||
|
||||
// Download the latest asset list and cache it
|
||||
AssetUtils.generateAssetCache().whenComplete((aVoid, ex) -> {
|
||||
if (ex != null) {
|
||||
return;
|
||||
}
|
||||
MinecraftLocale.ensureEN_US();
|
||||
String locale = GeyserLocale.getDefaultLocale();
|
||||
if (!"en_us".equals(locale)) {
|
||||
// English will be loaded after assets are downloaded, if necessary
|
||||
MinecraftLocale.downloadAndLoadLocale(locale);
|
||||
}
|
||||
|
||||
ProvidedSkins.init();
|
||||
|
||||
CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks);
|
||||
});
|
||||
|
||||
startInstance();
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ public class FakeHeadProvider {
|
|||
|
||||
SkinProvider.Skin skin = skinData.skin();
|
||||
SkinProvider.Cape cape = skinData.cape();
|
||||
SkinProvider.SkinGeometry geometry = skinData.geometry().getGeometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
||||
SkinProvider.SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
||||
? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL;
|
||||
|
||||
SkinProvider.Skin headSkin = SkinProvider.getOrDefault(
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.skin;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ProvidedSkin {
|
||||
@Getter private byte[] skin;
|
||||
|
||||
public ProvidedSkin(String internalUrl) {
|
||||
try {
|
||||
BufferedImage image;
|
||||
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(internalUrl)) {
|
||||
image = ImageIO.read(stream);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4);
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
int rgba = image.getRGB(x, y);
|
||||
outputStream.write((rgba >> 16) & 0xFF); // Red
|
||||
outputStream.write((rgba >> 8) & 0xFF); // Green
|
||||
outputStream.write(rgba & 0xFF); // Blue
|
||||
outputStream.write((rgba >> 24) & 0xFF); // Alpha
|
||||
}
|
||||
}
|
||||
image.flush();
|
||||
skin = outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normal file
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.skin;
|
||||
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.util.AssetUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ProvidedSkins {
|
||||
private static final ProvidedSkin[] PROVIDED_SKINS = {
|
||||
new ProvidedSkin("textures/entity/player/slim/alex.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/ari.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/efe.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/kai.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/makena.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/noor.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/steve.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/sunny.png", true),
|
||||
new ProvidedSkin("textures/entity/player/slim/zuri.png", true),
|
||||
new ProvidedSkin("textures/entity/player/wide/alex.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/ari.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/efe.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/kai.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/makena.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/noor.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/steve.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/sunny.png", false),
|
||||
new ProvidedSkin("textures/entity/player/wide/zuri.png", false)
|
||||
};
|
||||
|
||||
public static ProvidedSkin getDefaultPlayerSkin(UUID uuid) {
|
||||
return PROVIDED_SKINS[Math.floorMod(uuid.hashCode(), PROVIDED_SKINS.length)];
|
||||
}
|
||||
|
||||
private ProvidedSkins() {
|
||||
}
|
||||
|
||||
public static final class ProvidedSkin {
|
||||
private SkinProvider.Skin data;
|
||||
private final boolean slim;
|
||||
|
||||
ProvidedSkin(String asset, boolean slim) {
|
||||
this.slim = slim;
|
||||
|
||||
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
|
||||
.resolve("cache")
|
||||
.resolve("default_player_skins")
|
||||
.resolve(slim ? "slim" : "wide");
|
||||
String assetName = asset.substring(asset.lastIndexOf('/') + 1);
|
||||
|
||||
File location = folder.resolve(assetName).toFile();
|
||||
AssetUtils.addTask(!location.exists(), new AssetUtils.ClientJarTask("assets/minecraft/" + asset,
|
||||
(stream) -> AssetUtils.saveFile(location, stream),
|
||||
() -> {
|
||||
try {
|
||||
// TODO lazy initialize?
|
||||
BufferedImage image;
|
||||
try (InputStream stream = new FileInputStream(location)) {
|
||||
image = ImageIO.read(stream);
|
||||
}
|
||||
|
||||
byte[] byteData = SkinProvider.bufferedImageToImageData(image);
|
||||
image.flush();
|
||||
|
||||
String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide");
|
||||
this.data = new SkinProvider.Skin(-1, identifier, byteData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public SkinProvider.Skin getData() {
|
||||
// Fall back to the default skin if we can't load our skins, or it's not loaded yet.
|
||||
return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN);
|
||||
}
|
||||
|
||||
public boolean isSlim() {
|
||||
return slim;
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
static {
|
||||
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
|
||||
.resolve("cache")
|
||||
.resolve("default_player_skins");
|
||||
folder.toFile().mkdirs();
|
||||
// Two directories since there are two skins for each model: one slim, one wide
|
||||
folder.resolve("slim").toFile().mkdir();
|
||||
folder.resolve("wide").toFile().mkdir();
|
||||
}
|
||||
}
|
|
@ -34,7 +34,6 @@ import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
|||
import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
|
||||
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.network.AuthType;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.auth.BedrockClientData;
|
||||
|
@ -54,13 +53,30 @@ public class SkinManager {
|
|||
* Builds a Bedrock player list entry from our existing, cached Bedrock skin information
|
||||
*/
|
||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
||||
// First: see if we have the cached skin texture ID.
|
||||
GameProfileData data = GameProfileData.from(playerEntity);
|
||||
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.capeUrl());
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
||||
SkinProvider.Skin skin = null;
|
||||
SkinProvider.Cape cape = null;
|
||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.WIDE;
|
||||
if (data != null) {
|
||||
// GameProfileData is not null = server provided us with textures data to work with.
|
||||
skin = SkinProvider.getCachedSkin(data.skinUrl());
|
||||
cape = SkinProvider.getCachedCape(data.capeUrl());
|
||||
geometry = data.isAlex() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||
}
|
||||
|
||||
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.skinUrl());
|
||||
if (skin == null) {
|
||||
skin = SkinProvider.EMPTY_SKIN;
|
||||
if (skin == null || cape == null) {
|
||||
// The server either didn't have a texture to send, or we didn't have the texture ID cached.
|
||||
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||
// Otherwise, grab the default player skin
|
||||
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity);
|
||||
if (skin == null) {
|
||||
skin = fallbackSkinData.skin();
|
||||
geometry = fallbackSkinData.geometry();
|
||||
}
|
||||
if (cape == null) {
|
||||
cape = fallbackSkinData.cape();
|
||||
}
|
||||
}
|
||||
|
||||
return buildEntryManually(
|
||||
|
@ -144,10 +160,10 @@ public class SkinManager {
|
|||
}
|
||||
|
||||
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
|
||||
return SerializedSkin.of(skinId, "", geometry.getGeometryName(),
|
||||
return SerializedSkin.of(skinId, "", geometry.geometryName(),
|
||||
ImageData.of(skin.getSkinData()), Collections.emptyList(),
|
||||
ImageData.of(cape.getCapeData()), geometry.getGeometryData(),
|
||||
"", true, false, false, cape.getCapeId(), skinId);
|
||||
ImageData.of(cape.capeData()), geometry.geometryData(),
|
||||
"", true, false, false, cape.capeId(), skinId);
|
||||
}
|
||||
|
||||
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
||||
|
@ -193,7 +209,7 @@ public class SkinManager {
|
|||
}
|
||||
|
||||
if (!clientData.getCapeId().equals("")) {
|
||||
SkinProvider.storeBedrockCape(playerEntity.getUuid(), capeBytes);
|
||||
SkinProvider.storeBedrockCape(clientData.getCapeId(), capeBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
|
||||
|
@ -238,26 +254,21 @@ public class SkinManager {
|
|||
* @param entity entity to build the GameProfileData from
|
||||
* @return The built GameProfileData
|
||||
*/
|
||||
public static GameProfileData from(PlayerEntity entity) {
|
||||
public static @Nullable GameProfileData from(PlayerEntity entity) {
|
||||
try {
|
||||
String texturesProperty = entity.getTexturesProperty();
|
||||
|
||||
if (texturesProperty == null) {
|
||||
// Likely offline mode
|
||||
return loadBedrockOrOfflineSkin(entity);
|
||||
}
|
||||
GameProfileData data = loadFromJson(texturesProperty);
|
||||
if (data != null) {
|
||||
return data;
|
||||
} else {
|
||||
return loadBedrockOrOfflineSkin(entity);
|
||||
return null;
|
||||
}
|
||||
return loadFromJson(texturesProperty);
|
||||
} catch (IOException exception) {
|
||||
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername());
|
||||
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
return loadBedrockOrOfflineSkin(entity);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,27 +297,5 @@ public class SkinManager {
|
|||
|
||||
return new GameProfileData(skinUrl, capeUrl, isAlex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return default skin with default cape when texture data is invalid, or the Bedrock player's skin if this
|
||||
* is a Bedrock player.
|
||||
*/
|
||||
private static GameProfileData loadBedrockOrOfflineSkin(PlayerEntity entity) {
|
||||
// Fallback to the offline mode of working it out
|
||||
UUID uuid = entity.getUuid();
|
||||
boolean isAlex = (Math.abs(uuid.hashCode() % 2) == 1);
|
||||
|
||||
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
|
||||
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
|
||||
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
|
||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||
|
||||
if (session != null) {
|
||||
skinUrl = session.getClientData().getSkinId();
|
||||
capeUrl = session.getClientData().getCapeId();
|
||||
}
|
||||
}
|
||||
return new GameProfileData(skinUrl, capeUrl, isAlex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,22 +26,25 @@
|
|||
package org.geysermc.geyser.skin;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrays;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.network.AuthType;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import org.geysermc.geyser.util.FileUtils;
|
||||
import org.geysermc.geyser.util.WebUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
@ -57,28 +60,28 @@ import java.util.concurrent.*;
|
|||
import java.util.function.Predicate;
|
||||
|
||||
public class SkinProvider {
|
||||
public static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
|
||||
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
|
||||
static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
|
||||
|
||||
public static final byte[] STEVE_SKIN = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin();
|
||||
public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN);
|
||||
public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
|
||||
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
|
||||
private static final Map<String, Skin> permanentSkins = new HashMap<>() {{
|
||||
put("steve", EMPTY_SKIN);
|
||||
put("alex", EMPTY_SKIN_ALEX);
|
||||
}};
|
||||
private static final Cache<String, Skin> cachedSkins = CacheBuilder.newBuilder()
|
||||
static final Skin EMPTY_SKIN;
|
||||
static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true);
|
||||
|
||||
private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build();
|
||||
private static final Cache<String, Skin> CACHED_JAVA_SKINS = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build();
|
||||
|
||||
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
|
||||
|
||||
public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true);
|
||||
private static final Cache<String, Cape> cachedCapes = CacheBuilder.newBuilder()
|
||||
private static final Cache<String, Cape> CACHED_BEDROCK_CAPES = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build();
|
||||
private static final Cache<String, Skin> CACHED_BEDROCK_SKINS = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build();
|
||||
|
||||
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
|
||||
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
|
||||
|
||||
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
|
||||
|
||||
|
@ -86,18 +89,36 @@ public class SkinProvider {
|
|||
* Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and
|
||||
* offline mode players use version 3.
|
||||
*/
|
||||
public static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
|
||||
private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
|
||||
|
||||
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
|
||||
public static final String EARS_GEOMETRY;
|
||||
public static final String EARS_GEOMETRY_SLIM;
|
||||
public static final SkinGeometry SKULL_GEOMETRY;
|
||||
public static final SkinGeometry WEARING_CUSTOM_SKULL;
|
||||
public static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
|
||||
|
||||
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
|
||||
private static final String EARS_GEOMETRY;
|
||||
private static final String EARS_GEOMETRY_SLIM;
|
||||
static final SkinGeometry SKULL_GEOMETRY;
|
||||
static final SkinGeometry WEARING_CUSTOM_SKULL;
|
||||
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
|
||||
|
||||
static {
|
||||
// Generate the empty texture to use as an emergency fallback
|
||||
final int pink = -524040;
|
||||
final int black = -16777216;
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 4 + 64 * 4);
|
||||
for (int y = 0; y < 64; y++) {
|
||||
for (int x = 0; x < 64; x++) {
|
||||
int rgba;
|
||||
if (y > 32) {
|
||||
rgba = x >= 32 ? pink : black;
|
||||
} else {
|
||||
rgba = x >= 32 ? black : pink;
|
||||
}
|
||||
outputStream.write((rgba >> 16) & 0xFF); // Red
|
||||
outputStream.write((rgba >> 8) & 0xFF); // Green
|
||||
outputStream.write(rgba & 0xFF); // Blue
|
||||
outputStream.write((rgba >> 24) & 0xFF); // Alpha
|
||||
}
|
||||
}
|
||||
EMPTY_SKIN = new Skin(-1, "geysermc:empty", outputStream.toByteArray());
|
||||
|
||||
/* Load in the normal ears geometry */
|
||||
EARS_GEOMETRY = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.ears.json"), StandardCharsets.UTF_8);
|
||||
|
||||
|
@ -141,48 +162,104 @@ public class SkinProvider {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean hasCapeCached(String capeUrl) {
|
||||
return cachedCapes.getIfPresent(capeUrl) != null;
|
||||
/**
|
||||
* Search our cached database for an already existing, translated skin of this Java URL.
|
||||
*/
|
||||
static Skin getCachedSkin(String skinUrl) {
|
||||
return CACHED_JAVA_SKINS.getIfPresent(skinUrl);
|
||||
}
|
||||
|
||||
public static Skin getCachedSkin(String skinUrl) {
|
||||
return permanentSkins.getOrDefault(skinUrl, cachedSkins.getIfPresent(skinUrl));
|
||||
/**
|
||||
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
|
||||
*/
|
||||
static SkinData determineFallbackSkinData(PlayerEntity entity) {
|
||||
Skin skin = null;
|
||||
Cape cape = null;
|
||||
SkinGeometry geometry = SkinGeometry.WIDE;
|
||||
|
||||
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
|
||||
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||
UUID uuid = entity.getUuid();
|
||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||
if (session != null) {
|
||||
String skinId = session.getClientData().getSkinId();
|
||||
skin = CACHED_BEDROCK_SKINS.getIfPresent(skinId);
|
||||
String capeId = session.getClientData().getCapeId();
|
||||
cape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
|
||||
geometry = cachedGeometry.getOrDefault(uuid, geometry);
|
||||
}
|
||||
}
|
||||
|
||||
if (skin == null) {
|
||||
// We don't have a skin for the player right now. Fall back to a default.
|
||||
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid());
|
||||
skin = providedSkin.getData();
|
||||
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||
}
|
||||
|
||||
if (cape == null) {
|
||||
cape = EMPTY_CAPE;
|
||||
}
|
||||
|
||||
return new SkinData(skin, cape, geometry);
|
||||
}
|
||||
|
||||
public static Cape getCachedCape(String capeUrl) {
|
||||
Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE;
|
||||
return cape != null ? cape : EMPTY_CAPE;
|
||||
/**
|
||||
* Used as a fallback if an official Java cape doesn't exist for this user.
|
||||
*/
|
||||
@Nonnull
|
||||
private static Cape getCachedBedrockCape(UUID uuid) {
|
||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||
if (session != null) {
|
||||
String capeId = session.getClientData().getCapeId();
|
||||
Cape bedrockCape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
|
||||
if (bedrockCape != null) {
|
||||
return bedrockCape;
|
||||
}
|
||||
}
|
||||
return EMPTY_CAPE;
|
||||
}
|
||||
|
||||
public static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
|
||||
@Nullable
|
||||
static Cape getCachedCape(String capeUrl) {
|
||||
if (capeUrl == null) {
|
||||
return null;
|
||||
}
|
||||
return CACHED_JAVA_CAPES.getIfPresent(capeUrl);
|
||||
}
|
||||
|
||||
static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
|
||||
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
|
||||
if (data == null) {
|
||||
// This player likely does not have a textures property
|
||||
return CompletableFuture.completedFuture(determineFallbackSkinData(entity));
|
||||
}
|
||||
|
||||
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
|
||||
.thenApplyAsync(skinAndCape -> {
|
||||
try {
|
||||
Skin skin = skinAndCape.getSkin();
|
||||
Cape cape = skinAndCape.getCape();
|
||||
SkinGeometry geometry = SkinGeometry.getLegacy(data.isAlex());
|
||||
Skin skin = skinAndCape.skin();
|
||||
Cape cape = skinAndCape.cape();
|
||||
SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
|
||||
|
||||
if (cape.isFailed()) {
|
||||
cape = getOrDefault(requestBedrockCape(entity.getUuid()),
|
||||
EMPTY_CAPE, 3);
|
||||
// Whether we should see if this player has a Bedrock skin we should check for on failure of
|
||||
// any skin property
|
||||
boolean checkForBedrock = entity.getUuid().version() != 4;
|
||||
|
||||
if (cape.failed() && checkForBedrock) {
|
||||
cape = getCachedBedrockCape(entity.getUuid());
|
||||
}
|
||||
|
||||
if (cape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||
cape = getOrDefault(requestUnofficialCape(
|
||||
cape, entity.getUuid(),
|
||||
entity.getUsername(), false
|
||||
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
|
||||
}
|
||||
|
||||
geometry = getOrDefault(requestBedrockGeometry(
|
||||
geometry, entity.getUuid()
|
||||
), geometry, 3);
|
||||
|
||||
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
|
||||
// Not a bedrock player check for ears
|
||||
if (geometry.isFailed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
|
||||
if (geometry.failed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
|
||||
boolean isEars;
|
||||
|
||||
// Its deadmau5, gotta support his skin :)
|
||||
|
@ -213,26 +290,17 @@ public class SkinProvider {
|
|||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
||||
}
|
||||
|
||||
return new SkinData(skinAndCape.getSkin(), skinAndCape.getCape(), null);
|
||||
return new SkinData(skinAndCape.skin(), skinAndCape.cape(), null);
|
||||
});
|
||||
}
|
||||
|
||||
public static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
|
||||
private static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
long time = System.currentTimeMillis();
|
||||
String newSkinUrl = skinUrl;
|
||||
|
||||
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
|
||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(playerId);
|
||||
|
||||
if (session != null) {
|
||||
newSkinUrl = session.getClientData().getSkinId();
|
||||
}
|
||||
}
|
||||
|
||||
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
|
||||
SkinAndCape skinAndCape = new SkinAndCape(
|
||||
getOrDefault(requestSkin(playerId, newSkinUrl, false), EMPTY_SKIN, 5),
|
||||
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
|
||||
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
|
||||
);
|
||||
|
||||
|
@ -241,7 +309,7 @@ public class SkinProvider {
|
|||
}, EXECUTOR_SERVICE);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
|
||||
static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
|
||||
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
|
||||
CompletableFuture<Skin> requestedSkin = requestedSkins.get(textureUrl);
|
||||
if (requestedSkin != null) {
|
||||
|
@ -249,7 +317,7 @@ public class SkinProvider {
|
|||
return requestedSkin;
|
||||
}
|
||||
|
||||
Skin cachedSkin = getCachedSkin(textureUrl);
|
||||
Skin cachedSkin = CACHED_JAVA_SKINS.getIfPresent(textureUrl);
|
||||
if (cachedSkin != null) {
|
||||
return CompletableFuture.completedFuture(cachedSkin);
|
||||
}
|
||||
|
@ -259,23 +327,26 @@ public class SkinProvider {
|
|||
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
|
||||
.whenCompleteAsync((skin, throwable) -> {
|
||||
skin.updated = true;
|
||||
cachedSkins.put(textureUrl, skin);
|
||||
CACHED_JAVA_SKINS.put(textureUrl, skin);
|
||||
requestedSkins.remove(textureUrl);
|
||||
});
|
||||
requestedSkins.put(textureUrl, future);
|
||||
} else {
|
||||
Skin skin = supplySkin(playerId, textureUrl);
|
||||
future = CompletableFuture.completedFuture(skin);
|
||||
cachedSkins.put(textureUrl, skin);
|
||||
CACHED_JAVA_SKINS.put(textureUrl, skin);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
|
||||
private static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
|
||||
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
||||
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
||||
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
|
||||
if (requestedCape != null) {
|
||||
return requestedCape;
|
||||
}
|
||||
|
||||
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
|
||||
Cape cachedCape = CACHED_JAVA_CAPES.getIfPresent(capeUrl);
|
||||
if (cachedCape != null) {
|
||||
return CompletableFuture.completedFuture(cachedCape);
|
||||
}
|
||||
|
@ -284,21 +355,21 @@ public class SkinProvider {
|
|||
if (newThread) {
|
||||
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
|
||||
.whenCompleteAsync((cape, throwable) -> {
|
||||
cachedCapes.put(capeUrl, cape);
|
||||
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||
requestedCapes.remove(capeUrl);
|
||||
});
|
||||
requestedCapes.put(capeUrl, future);
|
||||
} else {
|
||||
Cape cape = supplyCape(capeUrl, provider); // blocking
|
||||
future = CompletableFuture.completedFuture(cape);
|
||||
cachedCapes.put(capeUrl, cape);
|
||||
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
||||
private static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
||||
String username, boolean newThread) {
|
||||
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||
if (officialCape.failed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||
for (CapeProvider provider : CapeProvider.VALUES) {
|
||||
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
||||
continue;
|
||||
|
@ -308,7 +379,7 @@ public class SkinProvider {
|
|||
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
|
||||
EMPTY_CAPE, 4
|
||||
);
|
||||
if (!cape1.isFailed()) {
|
||||
if (!cape1.failed()) {
|
||||
return CompletableFuture.completedFuture(cape1);
|
||||
}
|
||||
}
|
||||
|
@ -316,7 +387,7 @@ public class SkinProvider {
|
|||
return CompletableFuture.completedFuture(officialCape);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
|
||||
private static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
|
||||
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
|
||||
|
||||
CompletableFuture<Skin> future;
|
||||
|
@ -339,7 +410,7 @@ public class SkinProvider {
|
|||
* @param newThread Should we start in a new thread
|
||||
* @return The updated skin with ears
|
||||
*/
|
||||
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
|
||||
private static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
|
||||
for (EarsProvider provider : EarsProvider.VALUES) {
|
||||
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
||||
continue;
|
||||
|
@ -357,30 +428,17 @@ public class SkinProvider {
|
|||
return CompletableFuture.completedFuture(officialSkin);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID) {
|
||||
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
|
||||
if (bedrockCape == null) {
|
||||
bedrockCape = EMPTY_CAPE;
|
||||
}
|
||||
return CompletableFuture.completedFuture(bedrockCape);
|
||||
static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) {
|
||||
Skin skin = new Skin(playerID, skinId, skinData, System.currentTimeMillis(), true, false);
|
||||
CACHED_BEDROCK_SKINS.put(skin.getTextureUrl(), skin);
|
||||
}
|
||||
|
||||
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) {
|
||||
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
|
||||
return CompletableFuture.completedFuture(bedrockGeometry);
|
||||
static void storeBedrockCape(String capeId, byte[] capeData) {
|
||||
Cape cape = new Cape(capeId, capeId, capeData, System.currentTimeMillis(), false);
|
||||
CACHED_BEDROCK_CAPES.put(capeId, cape);
|
||||
}
|
||||
|
||||
public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) {
|
||||
Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false);
|
||||
cachedSkins.put(skin.getTextureUrl(), skin);
|
||||
}
|
||||
|
||||
public static void storeBedrockCape(UUID playerID, byte[] capeData) {
|
||||
Cape cape = new Cape(playerID.toString() + ".Bedrock", playerID.toString(), capeData, System.currentTimeMillis(), false);
|
||||
cachedCapes.put(playerID.toString() + ".Bedrock", cape);
|
||||
}
|
||||
|
||||
public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
|
||||
static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
|
||||
SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData), false);
|
||||
cachedGeometry.put(playerID, geometry);
|
||||
}
|
||||
|
@ -391,7 +449,7 @@ public class SkinProvider {
|
|||
* @param skin The skin to cache
|
||||
*/
|
||||
public static void storeEarSkin(Skin skin) {
|
||||
cachedSkins.put(skin.getTextureUrl(), skin);
|
||||
CACHED_JAVA_SKINS.put(skin.getTextureUrl(), skin);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -400,7 +458,7 @@ public class SkinProvider {
|
|||
* @param playerID The UUID to cache it against
|
||||
* @param isSlim If the player is using an slim base
|
||||
*/
|
||||
public static void storeEarGeometry(UUID playerID, boolean isSlim) {
|
||||
private static void storeEarGeometry(UUID playerID, boolean isSlim) {
|
||||
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
|
||||
}
|
||||
|
||||
|
@ -414,7 +472,7 @@ public class SkinProvider {
|
|||
}
|
||||
|
||||
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
|
||||
byte[] cape = EMPTY_CAPE.getCapeData();
|
||||
byte[] cape = EMPTY_CAPE.capeData();
|
||||
try {
|
||||
cape = requestImage(capeUrl, provider);
|
||||
} catch (Exception ignored) {
|
||||
|
@ -604,7 +662,7 @@ public class SkinProvider {
|
|||
}
|
||||
|
||||
private static BufferedImage readFiveZigCape(String url) throws IOException {
|
||||
JsonNode element = OBJECT_MAPPER.readTree(WebUtils.getBody(url));
|
||||
JsonNode element = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(url));
|
||||
if (element != null && element.isObject()) {
|
||||
JsonNode capeElement = element.get("d");
|
||||
if (capeElement == null || capeElement.isNull()) return null;
|
||||
|
@ -683,13 +741,12 @@ public class SkinProvider {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class SkinAndCape {
|
||||
private final Skin skin;
|
||||
private final Cape cape;
|
||||
public record SkinAndCape(Skin skin, Cape cape) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a full package of skin, cape, and geometry.
|
||||
*/
|
||||
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
|
||||
}
|
||||
|
||||
|
@ -703,29 +760,19 @@ public class SkinProvider {
|
|||
private boolean updated;
|
||||
private boolean ears;
|
||||
|
||||
private Skin(long requestedOn, String textureUrl, byte[] skinData) {
|
||||
Skin(long requestedOn, String textureUrl, byte[] skinData) {
|
||||
this.requestedOn = requestedOn;
|
||||
this.textureUrl = textureUrl;
|
||||
this.skinData = skinData;
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class Cape {
|
||||
private final String textureUrl;
|
||||
private final String capeId;
|
||||
private final byte[] capeData;
|
||||
private final long requestedOn;
|
||||
private final boolean failed;
|
||||
public record Cape(String textureUrl, String capeId, byte[] capeData, long requestedOn, boolean failed) {
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class SkinGeometry {
|
||||
private final String geometryName;
|
||||
private final String geometryData;
|
||||
private final boolean failed;
|
||||
public record SkinGeometry(String geometryName, String geometryData, boolean failed) {
|
||||
public static SkinGeometry WIDE = getLegacy(false);
|
||||
public static SkinGeometry SLIM = getLegacy(true);
|
||||
|
||||
/**
|
||||
* Generate generic geometry
|
||||
|
@ -733,7 +780,7 @@ public class SkinProvider {
|
|||
* @param isSlim Should it be the alex model
|
||||
* @return The generic geometry object
|
||||
*/
|
||||
public static SkinGeometry getLegacy(boolean isSlim) {
|
||||
private static SkinGeometry getLegacy(boolean isSlim) {
|
||||
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
|
||||
}
|
||||
|
||||
|
@ -743,7 +790,7 @@ public class SkinProvider {
|
|||
* @param isSlim Should it be the alex model
|
||||
* @return The generated geometry for the ears model
|
||||
*/
|
||||
public static SkinGeometry getEars(boolean isSlim) {
|
||||
private static SkinGeometry getEars(boolean isSlim) {
|
||||
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,9 +42,9 @@ public class SkullSkinManager extends SkinManager {
|
|||
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
|
||||
skinId = skinId + "_skull";
|
||||
return SerializedSkin.of(
|
||||
skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
|
||||
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
|
||||
skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||
ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(),
|
||||
"", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,91 +25,45 @@
|
|||
|
||||
package org.geysermc.geyser.text;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.network.GameProtocol;
|
||||
import org.geysermc.geyser.util.AssetUtils;
|
||||
import org.geysermc.geyser.util.FileUtils;
|
||||
import org.geysermc.geyser.util.WebUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class MinecraftLocale {
|
||||
|
||||
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
|
||||
|
||||
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
|
||||
|
||||
private static VersionDownload clientJarInfo;
|
||||
|
||||
static {
|
||||
// Create the locales folder
|
||||
File localesFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales").toFile();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
localesFolder.mkdir();
|
||||
|
||||
// Download the latest asset list and cache it
|
||||
generateAssetCache().whenComplete((aVoid, ex) -> downloadAndLoadLocale(GeyserLocale.getDefaultLocale()));
|
||||
// FIXME TEMPORARY
|
||||
try {
|
||||
Files.delete(localesFolder.toPath().resolve("en_us.hash"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
|
||||
*/
|
||||
private static CompletableFuture<Void> generateAssetCache() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
// Get the version manifest from Mojang
|
||||
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
|
||||
|
||||
// Get the url for the latest version of the games manifest
|
||||
String latestInfoURL = "";
|
||||
for (Version version : versionManifest.getVersions()) {
|
||||
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
|
||||
latestInfoURL = version.getUrl();
|
||||
break;
|
||||
public static void ensureEN_US() {
|
||||
File localeFile = getFile("en_us");
|
||||
AssetUtils.addTask(!localeFile.exists(), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json",
|
||||
(stream) -> AssetUtils.saveFile(localeFile, stream),
|
||||
() -> {
|
||||
if ("en_us".equals(GeyserLocale.getDefaultLocale())) {
|
||||
loadLocale("en_us");
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we definitely got a version
|
||||
if (latestInfoURL.isEmpty()) {
|
||||
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
|
||||
}
|
||||
|
||||
// Get the individual version manifest
|
||||
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
|
||||
|
||||
// Get the client jar for use when downloading the en_us locale
|
||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
|
||||
clientJarInfo = versionInfo.getDownloads().get("client");
|
||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(clientJarInfo));
|
||||
|
||||
// Get the assets list
|
||||
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
|
||||
|
||||
// Put each asset into an array for use later
|
||||
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
|
||||
while (assetIterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = assetIterator.next();
|
||||
if (!entry.getKey().startsWith("minecraft/lang/")) {
|
||||
// No need to cache non-language assets as we don't use them
|
||||
continue;
|
||||
}
|
||||
|
||||
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
|
||||
ASSET_MAP.put(entry.getKey(), asset);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,7 +79,7 @@ public class MinecraftLocale {
|
|||
}
|
||||
|
||||
// Check the locale isn't already loaded
|
||||
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
|
||||
if (!AssetUtils.isAssetKnown("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
|
||||
if (loadLocale(locale)) {
|
||||
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
|
||||
} else {
|
||||
|
@ -148,33 +102,15 @@ public class MinecraftLocale {
|
|||
* @param locale Locale to download
|
||||
*/
|
||||
private static void downloadLocale(String locale) {
|
||||
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
|
||||
if (locale.equals("en_us")) {
|
||||
return;
|
||||
}
|
||||
File localeFile = getFile(locale);
|
||||
|
||||
// Check if we have already downloaded the locale file
|
||||
if (localeFile.exists()) {
|
||||
String curHash = "";
|
||||
String targetHash;
|
||||
|
||||
if (locale.equals("en_us")) {
|
||||
try {
|
||||
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toFile();
|
||||
if (hashFile.exists()) {
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
|
||||
curHash = br.readLine().trim();
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) { }
|
||||
|
||||
if (clientJarInfo == null) {
|
||||
// Likely failed to download
|
||||
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
|
||||
return;
|
||||
}
|
||||
targetHash = clientJarInfo.getSha1();
|
||||
} else {
|
||||
curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
|
||||
targetHash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
|
||||
}
|
||||
String curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
|
||||
String targetHash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
|
||||
|
||||
if (!curHash.equals(targetHash)) {
|
||||
GeyserImpl.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale);
|
||||
|
@ -184,22 +120,19 @@ public class MinecraftLocale {
|
|||
}
|
||||
}
|
||||
|
||||
// Create the en_us locale
|
||||
if (locale.equals("en_us")) {
|
||||
downloadEN_US(localeFile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the hash and download the locale
|
||||
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
|
||||
String hash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
|
||||
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error("Unable to download locale file hash", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getFile(String locale) {
|
||||
return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a locale already downloaded, if the file doesn't exist it just logs a warning
|
||||
*
|
||||
|
@ -254,51 +187,6 @@ public class MinecraftLocale {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download then en_us locale by downloading the server jar and extracting it from there.
|
||||
*
|
||||
* @param localeFile File to save the locale to
|
||||
*/
|
||||
private static void downloadEN_US(File localeFile) {
|
||||
try {
|
||||
// Let the user know we are downloading the JAR
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
|
||||
GeyserImpl.getInstance().getLogger().debug("Download URL: " + clientJarInfo.getUrl());
|
||||
|
||||
// Download the smallest JAR (client or server)
|
||||
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
|
||||
WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString());
|
||||
|
||||
// Load in the JAR as a zip and extract the file
|
||||
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
|
||||
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) {
|
||||
try (FileOutputStream outStream = new FileOutputStream(localeFile)) {
|
||||
|
||||
// Write the file to the locale dir
|
||||
byte[] buf = new byte[fileStream.available()];
|
||||
int length;
|
||||
while ((length = fileStream.read(buf)) != -1) {
|
||||
outStream.write(buf, 0, length);
|
||||
}
|
||||
|
||||
// Flush all changes to disk and cleanup
|
||||
outStream.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the latest jar hash
|
||||
FileUtils.writeFile(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray());
|
||||
|
||||
// Delete the nolonger needed client/server jar
|
||||
Files.delete(tmpFilePath);
|
||||
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given language string into the given locale, or falls back to the default locale
|
||||
*
|
||||
|
@ -333,111 +221,4 @@ public class MinecraftLocale {
|
|||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionManifest {
|
||||
@JsonProperty("latest")
|
||||
private LatestVersion latestVersion;
|
||||
|
||||
@JsonProperty("versions")
|
||||
private List<Version> versions;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class LatestVersion {
|
||||
@JsonProperty("release")
|
||||
private String release;
|
||||
|
||||
@JsonProperty("snapshot")
|
||||
private String snapshot;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class Version {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("time")
|
||||
private String time;
|
||||
|
||||
@JsonProperty("releaseTime")
|
||||
private String releaseTime;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionInfo {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("time")
|
||||
private String time;
|
||||
|
||||
@JsonProperty("releaseTime")
|
||||
private String releaseTime;
|
||||
|
||||
@JsonProperty("assetIndex")
|
||||
private AssetIndex assetIndex;
|
||||
|
||||
@JsonProperty("downloads")
|
||||
private Map<String, VersionDownload> downloads;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionDownload {
|
||||
@JsonProperty("sha1")
|
||||
private String sha1;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class AssetIndex {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("sha1")
|
||||
private String sha1;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
|
||||
@JsonProperty("totalSize")
|
||||
private int totalSize;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class Asset {
|
||||
@JsonProperty("hash")
|
||||
private String hash;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
}
|
||||
}
|
|
@ -79,10 +79,6 @@ public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<Clientbound
|
|||
playerEntity.setUsername(profile.getName());
|
||||
playerEntity.setTexturesProperty(texturesProperty);
|
||||
|
||||
|
||||
// We'll send our own PlayerListEntry in requestAndHandleSkinAndCape
|
||||
// But we need to send other player's entries so they show up in the player list
|
||||
// without processing their skin information - that'll be processed when they spawn in
|
||||
if (self) {
|
||||
SkinManager.requestAndHandleSkinAndCape(playerEntity, session, skinAndCape ->
|
||||
GeyserImpl.getInstance().getLogger().debug("Loaded Local Bedrock Java Skin Data for " + session.getClientData().getUsername()));
|
||||
|
|
329
core/src/main/java/org/geysermc/geyser/util/AssetUtils.java
Normal file
329
core/src/main/java/org/geysermc/geyser/util/AssetUtils.java
Normal file
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.network.GameProtocol;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* Implementation note: try to design processes to fail softly if the client jar can't be downloaded,
|
||||
* either if Mojang is down or internet access to Mojang is spotty.
|
||||
*/
|
||||
public final class AssetUtils {
|
||||
private static final String CLIENT_JAR_HASH_FILE = "client_jar.hash";
|
||||
|
||||
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
|
||||
|
||||
private static VersionDownload CLIENT_JAR_INFO;
|
||||
|
||||
private static final Queue<ClientJarTask> CLIENT_JAR_TASKS = new ArrayDeque<>();
|
||||
/**
|
||||
* Download the client jar even if the hash is correct
|
||||
*/
|
||||
private static boolean FORCE_DOWNLOAD_JAR = false;
|
||||
|
||||
public static Asset getAsset(String name) {
|
||||
return ASSET_MAP.get(name);
|
||||
}
|
||||
|
||||
public static boolean isAssetKnown(String name) {
|
||||
return ASSET_MAP.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add task to be ran after the client jar is downloaded or found to be cached.
|
||||
*
|
||||
* @param required if set to true, the client jar will always be downloaded, even if a pre-existing hash is matched.
|
||||
* This means an asset or texture is missing.
|
||||
*/
|
||||
public static void addTask(boolean required, ClientJarTask task) {
|
||||
CLIENT_JAR_TASKS.add(task);
|
||||
FORCE_DOWNLOAD_JAR |= required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
|
||||
*/
|
||||
public static CompletableFuture<Void> generateAssetCache() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
// Get the version manifest from Mojang
|
||||
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(
|
||||
WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
|
||||
|
||||
// Get the url for the latest version of the games manifest
|
||||
String latestInfoURL = "";
|
||||
for (Version version : versionManifest.getVersions()) {
|
||||
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
|
||||
latestInfoURL = version.getUrl();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we definitely got a version
|
||||
if (latestInfoURL.isEmpty()) {
|
||||
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
|
||||
}
|
||||
|
||||
// Get the individual version manifest
|
||||
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
|
||||
|
||||
// Get the client jar for use when downloading the en_us locale
|
||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
|
||||
CLIENT_JAR_INFO = versionInfo.getDownloads().get("client");
|
||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(CLIENT_JAR_INFO));
|
||||
|
||||
// Get the assets list
|
||||
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
|
||||
|
||||
// Put each asset into an array for use later
|
||||
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
|
||||
while (assetIterator.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = assetIterator.next();
|
||||
if (!entry.getKey().startsWith("minecraft/lang/")) {
|
||||
// No need to cache non-language assets as we don't use them
|
||||
continue;
|
||||
}
|
||||
|
||||
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
|
||||
ASSET_MAP.put(entry.getKey(), asset);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static void downloadAndRunClientJarTasks() {
|
||||
if (CLIENT_JAR_INFO == null) {
|
||||
// Likely failed to download
|
||||
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FORCE_DOWNLOAD_JAR) { // Don't bother checking the hash if we need to download new files anyway.
|
||||
String curHash = null;
|
||||
try {
|
||||
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve(CLIENT_JAR_HASH_FILE).toFile();
|
||||
if (hashFile.exists()) {
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
|
||||
curHash = br.readLine().trim();
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) { }
|
||||
String targetHash = CLIENT_JAR_INFO.getSha1();
|
||||
if (targetHash.equals(curHash)) {
|
||||
// Just run all tasks - no new download required
|
||||
ClientJarTask task;
|
||||
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
|
||||
task.whenDone.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Let the user know we are downloading the JAR
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
|
||||
GeyserImpl.getInstance().getLogger().debug("Download URL: " + CLIENT_JAR_INFO.getUrl());
|
||||
|
||||
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
|
||||
WebUtils.downloadFile(CLIENT_JAR_INFO.getUrl(), tmpFilePath.toString());
|
||||
|
||||
// Load in the JAR as a zip and extract the files
|
||||
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
|
||||
ClientJarTask task;
|
||||
while ((task = CLIENT_JAR_TASKS.poll()) != null) {
|
||||
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry(task.asset))) {
|
||||
task.ifNewDownload.accept(fileStream);
|
||||
task.whenDone.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the latest jar hash
|
||||
Path cache = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
|
||||
Files.createDirectories(cache);
|
||||
FileUtils.writeFile(cache.resolve(CLIENT_JAR_HASH_FILE).toString(), CLIENT_JAR_INFO.getSha1().toCharArray());
|
||||
|
||||
// Delete the nolonger needed client/server jar
|
||||
Files.delete(tmpFilePath);
|
||||
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveFile(File location, InputStream fileStream) throws IOException {
|
||||
try (FileOutputStream outStream = new FileOutputStream(location)) {
|
||||
|
||||
// Write the file to the locale dir
|
||||
byte[] buf = new byte[fileStream.available()];
|
||||
int length;
|
||||
while ((length = fileStream.read(buf)) != -1) {
|
||||
outStream.write(buf, 0, length);
|
||||
}
|
||||
|
||||
// Flush all changes to disk and cleanup
|
||||
outStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A process that requires we download the client jar.
|
||||
* Designed to accommodate Geyser updates that require more assets from the jar.
|
||||
*/
|
||||
public record ClientJarTask(String asset, InputStreamConsumer ifNewDownload, Runnable whenDone) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InputStreamConsumer {
|
||||
void accept(InputStream stream) throws IOException;
|
||||
}
|
||||
|
||||
/* Classes that map to JSON files served by Mojang */
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionManifest {
|
||||
@JsonProperty("latest")
|
||||
private LatestVersion latestVersion;
|
||||
|
||||
@JsonProperty("versions")
|
||||
private List<Version> versions;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class LatestVersion {
|
||||
@JsonProperty("release")
|
||||
private String release;
|
||||
|
||||
@JsonProperty("snapshot")
|
||||
private String snapshot;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class Version {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
|
||||
@JsonProperty("time")
|
||||
private String time;
|
||||
|
||||
@JsonProperty("releaseTime")
|
||||
private String releaseTime;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionInfo {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("time")
|
||||
private String time;
|
||||
|
||||
@JsonProperty("releaseTime")
|
||||
private String releaseTime;
|
||||
|
||||
@JsonProperty("assetIndex")
|
||||
private AssetIndex assetIndex;
|
||||
|
||||
@JsonProperty("downloads")
|
||||
private Map<String, VersionDownload> downloads;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class VersionDownload {
|
||||
@JsonProperty("sha1")
|
||||
private String sha1;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
static class AssetIndex {
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("sha1")
|
||||
private String sha1;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
|
||||
@JsonProperty("totalSize")
|
||||
private int totalSize;
|
||||
|
||||
@JsonProperty("url")
|
||||
private String url;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
public static class Asset {
|
||||
@JsonProperty("hash")
|
||||
private String hash;
|
||||
|
||||
@JsonProperty("size")
|
||||
private int size;
|
||||
}
|
||||
|
||||
private AssetUtils() {
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
|
@ -111,7 +111,7 @@ debug-mode: false
|
|||
|
||||
# Allow third party capes to be visible. Currently allowing:
|
||||
# OptiFine capes, LabyMod capes, 5Zig capes and MinecraftCapes
|
||||
allow-third-party-capes: true
|
||||
allow-third-party-capes: false
|
||||
|
||||
# Allow third party deadmau5 ears to be visible. Currently allowing:
|
||||
# MinecraftCapes
|
||||
|
|
Loading…
Reference in a new issue