mirror of
https://github.com/GeyserMC/Floodgate.git
synced 2026-01-04 15:31:48 +00:00
Stopped using Jackson, initial version of Skins and fixed a few things
This commit is contained in:
@@ -28,7 +28,9 @@ package org.geysermc.floodgate;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.geysermc.floodgate.api.FloodgateApi;
|
||||
import org.geysermc.floodgate.api.InstanceHolder;
|
||||
@@ -43,6 +45,7 @@ import org.geysermc.floodgate.util.RawSkin;
|
||||
import org.geysermc.floodgate.util.UiProfile;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FloodgatePlayerImpl implements FloodgatePlayer {
|
||||
private final String version;
|
||||
private final String username;
|
||||
@@ -55,6 +58,7 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer {
|
||||
private final UiProfile uiProfile;
|
||||
private final InputMode inputMode;
|
||||
private final String ip;
|
||||
private final boolean fromProxy; //todo remove hasBungeeData
|
||||
private final LinkedPlayer linkedPlayer;
|
||||
private final RawSkin rawSkin;
|
||||
|
||||
@@ -63,43 +67,45 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer {
|
||||
*/
|
||||
@Setter private boolean login = true;
|
||||
|
||||
FloodgatePlayerImpl(BedrockData data, RawSkin skin, String prefix, boolean replaceSpaces) {
|
||||
protected static FloodgatePlayerImpl from(BedrockData data, RawSkin skin,
|
||||
String prefix, boolean replaceSpaces) {
|
||||
FloodgateApi api = FloodgateApi.getInstance();
|
||||
version = data.getVersion();
|
||||
username = data.getUsername();
|
||||
|
||||
int usernameLength = Math.min(data.getUsername().length(), 16 - prefix.length());
|
||||
String editedUsername = prefix + data.getUsername().substring(0, usernameLength);
|
||||
|
||||
String javaUsername = prefix + data.getUsername().substring(0, usernameLength);
|
||||
if (replaceSpaces) {
|
||||
editedUsername = editedUsername.replaceAll(" ", "_");
|
||||
javaUsername = javaUsername.replaceAll(" ", "_");
|
||||
}
|
||||
javaUsername = editedUsername;
|
||||
javaUniqueId = api.createJavaPlayerId(Long.parseLong(data.getXuid()));
|
||||
|
||||
xuid = data.getXuid();
|
||||
deviceOs = DeviceOs.getById(data.getDeviceOs());
|
||||
languageCode = data.getLanguageCode();
|
||||
uiProfile = UiProfile.getById(data.getUiProfile());
|
||||
inputMode = InputMode.getById(data.getInputMode());
|
||||
ip = data.getIp();
|
||||
rawSkin = skin;
|
||||
UUID javaUniqueId = api.createJavaPlayerId(Long.parseLong(data.getXuid()));
|
||||
|
||||
DeviceOs deviceOs = DeviceOs.getById(data.getDeviceOs());
|
||||
UiProfile uiProfile = UiProfile.getById(data.getUiProfile());
|
||||
InputMode inputMode = InputMode.getById(data.getInputMode());
|
||||
|
||||
LinkedPlayer linkedPlayer;
|
||||
|
||||
// we'll use the LinkedPlayer provided by Bungee or Velocity (if they included one)
|
||||
if (data.hasPlayerLink()) {
|
||||
linkedPlayer = data.getLinkedPlayer();
|
||||
return;
|
||||
} else {
|
||||
// every implementation (Bukkit, Bungee and Velocity) run this constructor async,
|
||||
// so we should be fine doing this synchronised.
|
||||
linkedPlayer = fetchLinkedPlayer(api.getPlayerLink(), javaUniqueId);
|
||||
}
|
||||
|
||||
// every implementation (Bukkit, Bungee and Velocity) run this constructor async,
|
||||
// so we should be fine doing this synchronised.
|
||||
linkedPlayer = fetchLinkedPlayer(api.getPlayerLink());
|
||||
FloodgatePlayerImpl player = new FloodgatePlayerImpl(
|
||||
data.getVersion(), data.getUsername(), javaUsername, javaUniqueId, data.getXuid(),
|
||||
deviceOs, data.getLanguageCode(), uiProfile, inputMode, data.getIp(),
|
||||
data.isFromProxy(), linkedPlayer, skin);
|
||||
|
||||
// oh oh, now our encrypted data is incorrect. We have to update it...
|
||||
// oh oh, after fetching the linkedPlayer our encrypted data is incorrect.
|
||||
// We have to update it...
|
||||
if (linkedPlayer != null && api instanceof ProxyFloodgateApi) {
|
||||
InstanceHolder.castApi(ProxyFloodgateApi.class)
|
||||
.updateEncryptedData(getCorrectUniqueId(), toBedrockData());
|
||||
.updateEncryptedData(player.getCorrectUniqueId(), player.toBedrockData());
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
public UUID getCorrectUniqueId() {
|
||||
@@ -115,9 +121,9 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer {
|
||||
* Please note that this method loads the LinkedPlayer synchronously.
|
||||
*
|
||||
* @return LinkedPlayer or null if the player isn't linked or linking isn't enabled
|
||||
* @see #fetchLinkedPlayerAsync(PlayerLink) for the asynchronously alternative
|
||||
* @see #fetchLinkedPlayerAsync(PlayerLink, UUID) for the asynchronously alternative
|
||||
*/
|
||||
public LinkedPlayer fetchLinkedPlayer(PlayerLink link) {
|
||||
public static LinkedPlayer fetchLinkedPlayer(PlayerLink link, UUID javaUniqueId) {
|
||||
if (!link.isEnabledAndAllowed()) {
|
||||
return null;
|
||||
}
|
||||
@@ -135,18 +141,18 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer {
|
||||
*
|
||||
* @return a future holding the LinkedPlayer or null if the player isn't linked or when linking
|
||||
* isn't enabled
|
||||
* @see #fetchLinkedPlayer(PlayerLink) for the sync version
|
||||
* @see #fetchLinkedPlayer(PlayerLink, UUID) for the sync version
|
||||
*/
|
||||
public CompletableFuture<LinkedPlayer> fetchLinkedPlayerAsync(PlayerLink link) {
|
||||
public static CompletableFuture<LinkedPlayer> fetchLinkedPlayerAsync(PlayerLink link,
|
||||
UUID javaUniqueId) {
|
||||
return link.isEnabledAndAllowed() ?
|
||||
link.getLinkedPlayer(javaUniqueId) :
|
||||
CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
public BedrockData toBedrockData() {
|
||||
return new BedrockData(
|
||||
return BedrockData.of(
|
||||
version, username, xuid, deviceOs.ordinal(), languageCode,
|
||||
uiProfile.ordinal(), inputMode.ordinal(), ip, linkedPlayer
|
||||
);
|
||||
uiProfile.ordinal(), inputMode.ordinal(), ip, linkedPlayer, fromProxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class HandshakeHandler {
|
||||
System.out.println(rawSkin);
|
||||
|
||||
FloodgatePlayer player =
|
||||
new FloodgatePlayerImpl(bedrockData, rawSkin, usernamePrefix, replaceSpaces);
|
||||
FloodgatePlayerImpl.from(bedrockData, rawSkin, usernamePrefix, replaceSpaces);
|
||||
api.addPlayer(player.getJavaUniqueId(), player);
|
||||
|
||||
return new HandshakeResult(ResultType.SUCCESS, dataArray, bedrockData, player);
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
|
||||
package org.geysermc.floodgate.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.security.Key;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -36,29 +34,19 @@ import lombok.Getter;
|
||||
*/
|
||||
@Getter
|
||||
public class FloodgateConfig {
|
||||
@JsonProperty(value = "key-file-name")
|
||||
private String keyFileName;
|
||||
@JsonProperty(value = "username-prefix")
|
||||
private String usernamePrefix;
|
||||
@JsonProperty(value = "replace-spaces")
|
||||
private boolean replaceSpaces;
|
||||
private boolean applySkinDirectly; //todo
|
||||
|
||||
@JsonProperty(value = "default-locale")
|
||||
private String defaultLocale;
|
||||
|
||||
@JsonProperty(value = "disconnect")
|
||||
private DisconnectMessages messages;
|
||||
|
||||
@JsonProperty(value = "player-link")
|
||||
private DisconnectMessages disconnect;
|
||||
private PlayerLinkConfig playerLink;
|
||||
|
||||
@JsonProperty
|
||||
private boolean debug;
|
||||
private int configVersion;
|
||||
|
||||
@JsonProperty("config-version")
|
||||
private boolean configVersion;
|
||||
|
||||
@JsonIgnore
|
||||
private Key key = null;
|
||||
|
||||
public void setKey(Key key) {
|
||||
@@ -73,23 +61,16 @@ public class FloodgateConfig {
|
||||
|
||||
@Getter
|
||||
public static class DisconnectMessages {
|
||||
@JsonProperty("invalid-key")
|
||||
private String invalidKey;
|
||||
@JsonProperty("invalid-arguments-length")
|
||||
private String invalidArgumentsLength;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class PlayerLinkConfig {
|
||||
@JsonProperty("enable")
|
||||
private boolean enabled;
|
||||
@JsonProperty("allow-linking")
|
||||
private boolean allowLinking;
|
||||
@JsonProperty("link-code-timeout")
|
||||
private boolean allowed;
|
||||
private long linkCodeTimeout;
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
@JsonProperty("auto-download")
|
||||
private boolean autoDownload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,12 @@
|
||||
|
||||
package org.geysermc.floodgate.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* The Floodgate configuration used by proxy platforms, currently Velocity and Bungeecord.
|
||||
*/
|
||||
@Getter
|
||||
public final class ProxyFloodgateConfig extends FloodgateConfig {
|
||||
@Getter
|
||||
@JsonProperty(value = "send-floodgate-data")
|
||||
private boolean sendFloodgateData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 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/Floodgate
|
||||
*/
|
||||
|
||||
package org.geysermc.floodgate.config.loader;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.geysermc.floodgate.config.FloodgateConfig;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor;
|
||||
import org.yaml.snakeyaml.introspector.BeanAccess;
|
||||
import org.yaml.snakeyaml.introspector.FieldProperty;
|
||||
import org.yaml.snakeyaml.introspector.Property;
|
||||
import org.yaml.snakeyaml.introspector.PropertyUtils;
|
||||
|
||||
public class ConfigInitializer {
|
||||
private static final Yaml YAML;
|
||||
|
||||
static {
|
||||
Constructor constructor = new CustomClassLoaderConstructor(
|
||||
ConfigInitializer.class.getClassLoader());
|
||||
|
||||
constructor.setPropertyUtils(new PropertyUtils() {
|
||||
@Override
|
||||
protected Map<String, Property> getPropertiesMap(Class<?> type, BeanAccess bAccess) {
|
||||
Map<String, Property> properties = new LinkedHashMap<>();
|
||||
Class<?> current = type;
|
||||
// make ProxyFloodgateConfig work
|
||||
while (FloodgateConfig.class.isAssignableFrom(current)) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
|
||||
String correctName = getCorrectName(field.getName());
|
||||
// children should override parents
|
||||
properties.putIfAbsent(correctName, new FieldProperty(field));
|
||||
}
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private String getCorrectName(String name) {
|
||||
// convert sendFloodgateData to send-floodgate-data,
|
||||
// which is the style of writing config fields
|
||||
StringBuilder propertyBuilder = new StringBuilder();
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char current = name.charAt(i);
|
||||
if (Character.isUpperCase(current)) {
|
||||
propertyBuilder.append('-').append(Character.toLowerCase(current));
|
||||
} else {
|
||||
propertyBuilder.append(current);
|
||||
}
|
||||
}
|
||||
return propertyBuilder.toString();
|
||||
}
|
||||
});
|
||||
YAML = new Yaml(constructor);
|
||||
}
|
||||
|
||||
public static <T extends FloodgateConfig> T initializeFrom(InputStream dataStream,
|
||||
Class<T> configClass) {
|
||||
return YAML.loadAs(dataStream, configClass);
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,6 @@
|
||||
|
||||
package org.geysermc.floodgate.config.loader;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -114,9 +112,8 @@ public final class ConfigLoader {
|
||||
updater.update(defaultConfigPath);
|
||||
}
|
||||
|
||||
FloodgateConfig config =
|
||||
new ObjectMapper(new YAMLFactory())
|
||||
.readValue(Files.readAllBytes(configPath), configClass);
|
||||
FloodgateConfig config = ConfigInitializer.initializeFrom(
|
||||
Files.newInputStream(configPath), configClass);
|
||||
|
||||
try {
|
||||
configInstance = (T) config;
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -57,8 +58,7 @@ public final class ConfigUpdater {
|
||||
}
|
||||
|
||||
Map<String, Object> config = new Yaml().load(configReader);
|
||||
// currently unused, but can be used when a config name has been changed
|
||||
Map<String, String> renames = new HashMap<>(0);
|
||||
Map<String, String> renames = new HashMap<>();
|
||||
|
||||
Object versionElement = config.get("config-version");
|
||||
// not a pre-rewrite config
|
||||
@@ -84,6 +84,8 @@ public final class ConfigUpdater {
|
||||
"(across all your servers, including Geyser)." +
|
||||
"We'll still try to update the config," +
|
||||
"but please regenerate the keys if it failed before asking for support.");
|
||||
renames.put("enabled", "enable"); //todo make dump system and add a boolean 'found-legacy-key' or something like that
|
||||
renames.put("allowed", "allow-linking");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -50,7 +50,7 @@ public abstract class CommonPlayerLink implements PlayerLink {
|
||||
private void init(FloodgateConfig config) {
|
||||
FloodgateConfig.PlayerLinkConfig linkConfig = config.getPlayerLink();
|
||||
enabled = linkConfig.isEnabled();
|
||||
allowLinking = linkConfig.isAllowLinking();
|
||||
allowLinking = linkConfig.isAllowed();
|
||||
verifyLinkTimeout = linkConfig.getLinkCodeTimeout();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.geysermc.common.form.Form;
|
||||
import org.geysermc.floodgate.util.RawSkin;
|
||||
|
||||
public abstract class PluginMessageHandler {
|
||||
protected final Short2ObjectMap<Form> storedForms = new Short2ObjectOpenHashMap<>();
|
||||
@@ -39,6 +40,10 @@ public abstract class PluginMessageHandler {
|
||||
|
||||
public abstract boolean sendForm(UUID player, Form form);
|
||||
|
||||
public abstract boolean sendSkinRequest(UUID player, RawSkin skin);
|
||||
|
||||
public abstract void sendSkinResponse(UUID player, String response);
|
||||
|
||||
protected byte[] createFormData(Form form) {
|
||||
short formId = getNextFormId();
|
||||
if (proxy) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 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/Floodgate
|
||||
*/
|
||||
|
||||
package org.geysermc.floodgate.skin;
|
||||
|
||||
import static org.geysermc.floodgate.util.MessageFormatter.format;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.geysermc.floodgate.api.logger.FloodgateLogger;
|
||||
import org.geysermc.floodgate.api.player.FloodgatePlayer;
|
||||
import org.geysermc.floodgate.platform.pluginmessage.PluginMessageHandler;
|
||||
import org.geysermc.floodgate.skin.SkinUploader.UploadResult;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class SkinHandler {
|
||||
private final SkinUploader uploader = new SkinUploader();
|
||||
private final PluginMessageHandler messageHandler;
|
||||
private final FloodgateLogger logger;
|
||||
|
||||
public final void handleSkinUploadFor(FloodgatePlayer player) {
|
||||
uploader.uploadSkin(player.getRawSkin())
|
||||
.whenComplete((uploadResult, throwable) -> {
|
||||
if (throwable != null) {
|
||||
logger.error(
|
||||
"Failed to upload player skin for " + player.getCorrectUsername(),
|
||||
throwable);
|
||||
|
||||
messageHandler.sendSkinResponse(
|
||||
player.getJavaUniqueId(), throwable.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadResult.getError() != null) {
|
||||
logger.error(format(
|
||||
"Error while uploading player skin for {}: {}",
|
||||
player.getCorrectUsername(), uploadResult.getError()));
|
||||
|
||||
messageHandler.sendSkinResponse(
|
||||
player.getJavaUniqueId(), uploadResult.getError());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Skin upload successful for " + player.getCorrectUsername());
|
||||
logger.info(uploadResult.getResponse().toString());
|
||||
messageHandler.sendSkinResponse(
|
||||
player.getJavaUniqueId(), uploadResult.getResponse().toString());
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void applySkin(FloodgatePlayer player, UploadResult result);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 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/Floodgate
|
||||
*/
|
||||
|
||||
package org.geysermc.floodgate.skin;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.imageio.ImageIO;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import org.geysermc.floodgate.util.RawSkin;
|
||||
|
||||
public final class SkinUploader {
|
||||
private static final String UPLOAD_URL = "https://api.mineskin.org/generate/upload";
|
||||
private static final String USER_AGENT = "GeyserMC/Floodgate";
|
||||
private static final MediaType PNG_TYPE = MediaType.get("image/png");
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private final Executor requestExecutor = Executors.newSingleThreadExecutor();
|
||||
private final OkHttpClient httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private long nextResult = 0;
|
||||
|
||||
public CompletableFuture<UploadResult> uploadSkin(RawSkin rawSkin) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
if (System.currentTimeMillis() < nextResult) {
|
||||
try {
|
||||
Thread.sleep(nextResult - System.currentTimeMillis());
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
SkinModel model = rawSkin.alex ? SkinModel.ALEX : SkinModel.STEVE;
|
||||
|
||||
BufferedImage image = SkinUtils.toBufferedImage(rawSkin);
|
||||
// a black 32x32 png was 133 bytes, so we assume that it's at least 133 bytes
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream(133);
|
||||
try {
|
||||
ImageIO.write(image, "png", stream);
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
|
||||
RequestBody fileData = RequestBody.create(PNG_TYPE, stream.toByteArray());
|
||||
|
||||
RequestBody body = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", "skin.png", fileData)
|
||||
.build();
|
||||
|
||||
System.out.println("final file stream size: " + stream.size());
|
||||
|
||||
Buffer buffer = new Buffer();
|
||||
try {
|
||||
body.writeTo(buffer);
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(UPLOAD_URL + getUploadUrlParameters(model))
|
||||
.header(HttpHeaders.USER_AGENT, USER_AGENT)
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
if (response.body() == null) {
|
||||
throw new RuntimeException("Response didn't have a body!");
|
||||
}
|
||||
return parseAndHandleResponse(response.code(), response.body().source());
|
||||
} catch (IOException exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
},
|
||||
requestExecutor
|
||||
);
|
||||
}
|
||||
|
||||
private String getUploadUrlParameters(SkinModel model) {
|
||||
return "?visibility=1&model=" + model.name;
|
||||
}
|
||||
|
||||
private UploadResult parseAndHandleResponse(int httpCode, BufferedSource response) {
|
||||
InputStreamReader reader = new InputStreamReader(response.inputStream());
|
||||
JsonObject jsonResponse = GSON.fromJson(reader, JsonObject.class);
|
||||
|
||||
if (jsonResponse == null) {
|
||||
throw new IllegalStateException("Response cannot be null!");
|
||||
}
|
||||
|
||||
System.out.println(jsonResponse.toString());
|
||||
|
||||
nextResult = jsonResponse.get("nextRequest").getAsLong();
|
||||
|
||||
if (httpCode >= 200 && httpCode < 300) {
|
||||
return UploadResult.success(httpCode, jsonResponse);
|
||||
} else {
|
||||
return UploadResult.failed(httpCode, jsonResponse.get("error").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public enum SkinModel {
|
||||
STEVE, ALEX;
|
||||
|
||||
public static final SkinModel[] VALUES = values();
|
||||
private final String name = name().toLowerCase();
|
||||
|
||||
public static SkinModel getByOrdinal(int ordinal) {
|
||||
return VALUES.length > ordinal ? VALUES[ordinal] : STEVE;
|
||||
}
|
||||
|
||||
public static SkinModel getByName(String name) {
|
||||
return name == null || !name.equalsIgnoreCase("alex") ? STEVE : ALEX;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class UploadResult {
|
||||
private final int httpCode;
|
||||
private final String error;
|
||||
|
||||
private final SkinModel model;
|
||||
private final String skinUrl;
|
||||
private final String capeUrl;
|
||||
private final JsonObject response;
|
||||
|
||||
public static UploadResult failed(int httpCode, String error) {
|
||||
return new UploadResult(httpCode, error, SkinModel.STEVE, null, null, null);
|
||||
}
|
||||
|
||||
public static UploadResult success(int httpCode, JsonObject body) {
|
||||
SkinModel model = SkinModel.getByName(body.get("model").getAsString());
|
||||
|
||||
JsonObject data = body.getAsJsonObject("data");
|
||||
JsonObject textureData = data.getAsJsonObject("texture");
|
||||
|
||||
JsonObject urls = textureData.getAsJsonObject("urls");
|
||||
String skinUrl = urls.get("skin").getAsString();
|
||||
String capeUrl = urls.has("cape") ? urls.get("cape").getAsString() : null;
|
||||
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("value", textureData.get("value").getAsString());
|
||||
response.addProperty("signature", textureData.get("signature").getAsString());
|
||||
|
||||
return new UploadResult(httpCode, null, model, skinUrl, capeUrl, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2020 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/Floodgate
|
||||
*/
|
||||
|
||||
package org.geysermc.floodgate.skin;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import org.geysermc.floodgate.util.RawSkin;
|
||||
|
||||
public class SkinUtils {
|
||||
/**
|
||||
* Get the ARGB int for a given index in some image data
|
||||
*
|
||||
* @param index Index to get
|
||||
* @param data Image data to find in
|
||||
* @return An int representing ARGB
|
||||
*/
|
||||
private static int getARGB(int index, byte[] data) {
|
||||
return (data[index + 3] & 0xFF) << 24 | (data[index] & 0xFF) << 16 |
|
||||
(data[index + 1] & 0xFF) << 8 | (data[index + 2] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a byte[] to a BufferedImage
|
||||
*
|
||||
* @param imageData The byte[] to convert
|
||||
* @param width The width of the target image
|
||||
* @param height The height of the target image
|
||||
* @return The converted BufferedImage
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(byte[] imageData, int width, int height) {
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
image.setRGB(x, y, getARGB((y * width + x) * 4, imageData));
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
public static BufferedImage toBufferedImage(RawSkin rawSkin) {
|
||||
return toBufferedImage(rawSkin.data, rawSkin.width, rawSkin.height);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public final class ReflectionUtils {
|
||||
@@ -45,7 +46,9 @@ public final class ReflectionUtils {
|
||||
* will become PacketHandshakingInSetProtocol if the prefix is set to
|
||||
* net.minecraft.server.v1_8R3
|
||||
*/
|
||||
@Setter private static String prefix = null;
|
||||
@Getter
|
||||
@Setter
|
||||
private static String prefix = null;
|
||||
|
||||
static {
|
||||
Field modifiersField = null;
|
||||
@@ -388,6 +391,14 @@ public final class ReflectionUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static <T> T cast(Object instance, Class<T> castTo) {
|
||||
if (castTo == null) {
|
||||
throw new IllegalArgumentException("Cannot cast instance to null");
|
||||
}
|
||||
return castTo.cast(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the given method of the given instance with the given arguments and cast the value.
|
||||
*
|
||||
|
||||
@@ -11,6 +11,10 @@ username-prefix: "*"
|
||||
# Should spaces be replaced with '_' in bedrock usernames?
|
||||
replace-spaces: true
|
||||
|
||||
# Should Floodgate apply the transferred Bedrock to Java skin directly to all Java players?
|
||||
# This might cause some
|
||||
apply-skin-directly: true
|
||||
|
||||
# The default locale for Floodgate. By default, Floodgate uses the system locale
|
||||
# default-locale: en_US
|
||||
|
||||
@@ -26,12 +30,12 @@ disconnect:
|
||||
player-link:
|
||||
# Whether to enable the linking system. Turning this off will prevent
|
||||
# players from using the linking feature even if they are already linked.
|
||||
enable: false
|
||||
enabled: false
|
||||
# Whether to allow the use of /linkaccount and /unlinkaccount
|
||||
# You can also use allow specific people to use the commands using the
|
||||
# permissions floodgate.linkaccount and floodgate.unlinkaccount.
|
||||
# This is only for linking, already connected people will stay connected
|
||||
allow-linking: true
|
||||
allowed: true
|
||||
# The amount of time until a link code expires in seconds
|
||||
link-code-timeout: 300
|
||||
# The database type you want to use.
|
||||
|
||||
@@ -31,12 +31,12 @@ disconnect:
|
||||
player-link:
|
||||
# Whether to enable the linking system. Turning this off will prevent
|
||||
# players from using the linking feature even if they are already linked.
|
||||
enable: false
|
||||
enabled: false
|
||||
# Whether to allow the use of /linkaccount and /unlinkaccount
|
||||
# You can also use allow specific people to use the commands using the
|
||||
# permissions floodgate.linkaccount and floodgate.unlinkaccount.
|
||||
# This is only for linking, already connected people will stay connected
|
||||
allow-linking: true
|
||||
allowed: true
|
||||
# The amount of time until a link code expires in seconds
|
||||
link-code-timeout: 300
|
||||
# The database type you want to use.
|
||||
|
||||
Reference in New Issue
Block a user