Slim skin fix - thanks to @craftycodie and @DelofJ

!

Signed-off-by: TheKodeToad <TheKodeToad@proton.me>
This commit is contained in:
TheKodeToad 2022-12-25 11:40:49 +00:00
parent 5c96b1c628
commit cb32711077
8 changed files with 322 additions and 44 deletions

View File

@ -23,9 +23,12 @@ set(SRC
org/prismlauncher/utils/JsonParser.java org/prismlauncher/utils/JsonParser.java
org/prismlauncher/utils/Parameters.java org/prismlauncher/utils/Parameters.java
org/prismlauncher/utils/ReflectionUtils.java org/prismlauncher/utils/ReflectionUtils.java
org/prismlauncher/utils/UrlUtils.java org/prismlauncher/utils/api/MojangApi.java
org/prismlauncher/utils/api/Texture.java
org/prismlauncher/utils/logging/Level.java org/prismlauncher/utils/logging/Level.java
org/prismlauncher/utils/logging/Log.java org/prismlauncher/utils/logging/Log.java
org/prismlauncher/utils/url/UrlUtils.java
org/prismlauncher/utils/url/CustomUrlConnection.java
net/minecraft/Launcher.java net/minecraft/Launcher.java
) )
add_jar(NewLaunch ${SRC}) add_jar(NewLaunch ${SRC})

View File

@ -41,7 +41,7 @@ import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.net.URLStreamHandler; import java.net.URLStreamHandler;
import org.prismlauncher.utils.UrlUtils; import org.prismlauncher.utils.url.UrlUtils;
final class Handler extends URLStreamHandler { final class Handler extends URLStreamHandler {
@ -52,8 +52,14 @@ final class Handler extends URLStreamHandler {
@Override @Override
protected URLConnection openConnection(URL address, Proxy proxy) throws IOException { protected URLConnection openConnection(URL address, Proxy proxy) throws IOException {
address = SkinFix.redirect(address); URLConnection result;
return UrlUtils.openHttpConnection(address, proxy);
// try skin fix
result = SkinFix.openConnection(address, proxy);
if (result != null)
return result;
return UrlUtils.openConnection(address, proxy);
} }
} }

View File

@ -40,8 +40,8 @@ import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory; import java.net.URLStreamHandlerFactory;
import org.prismlauncher.utils.Base64; import org.prismlauncher.utils.Base64;
import org.prismlauncher.utils.UrlUtils;
import org.prismlauncher.utils.logging.Log; import org.prismlauncher.utils.logging.Log;
import org.prismlauncher.utils.url.UrlUtils;
/** /**
* Fixes skins by redirecting to other URLs. * Fixes skins by redirecting to other URLs.

View File

@ -1,63 +1,85 @@
package org.prismlauncher.fix.online; package org.prismlauncher.fix.online;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.Proxy;
import java.net.URL; import java.net.URL;
import java.util.Map; import java.net.URLConnection;
import org.prismlauncher.utils.Base64; import javax.imageio.ImageIO;
import org.prismlauncher.utils.JsonParser;
import org.prismlauncher.utils.api.MojangApi;
import org.prismlauncher.utils.api.Texture;
import org.prismlauncher.utils.url.CustomUrlConnection;
import org.prismlauncher.utils.url.UrlUtils;
@SuppressWarnings("unchecked")
final class SkinFix { final class SkinFix {
static URL redirect(URL address) throws IOException { static URLConnection openConnection(URL address, Proxy proxy) throws IOException {
String skinOwner = findSkinOwner(address); String skinOwner = findSkinOwner(address);
if (skinOwner != null) if (skinOwner != null)
return convert(skinOwner, "SKIN"); // we need to correct the skin
return getSkinConnection(skinOwner, proxy);
String capeOwner = findCapeOwner(address); String capeOwner = findCapeOwner(address);
if (capeOwner != null) if (capeOwner != null) {
return convert(capeOwner, "CAPE"); // since we do not need to process the image, open a direct connection bypassing Handler
Texture texture = MojangApi.getTexture(MojangApi.getUuid(capeOwner), "CAPE");
return address; if (texture == null)
}
private static URL convert(String owner, String name) throws IOException {
Map<String, Object> textures = getTextures(owner);
if (textures != null) {
textures = (Map<String, Object>) textures.get(name);
if (textures == null)
return null; return null;
return new URL((String) textures.get("url")); return UrlUtils.openConnection(texture.getUrl(), proxy);
} }
return null; return null;
} }
private static Map<String, Object> getTextures(String owner) throws IOException { private static URLConnection getSkinConnection(String owner, Proxy proxy) throws IOException {
try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + owner).openStream()) { Texture texture = MojangApi.getTexture(MojangApi.getUuid(owner), "SKIN");
Map<String, Object> map = (Map<String, Object>) JsonParser.parse(in); if (texture == null)
String id = (String) map.get("id");
try (InputStream profileIn = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + id)
.openStream()) {
Map<String, Object> profile = (Map<String, Object>) JsonParser.parse(profileIn);
for (Map<String, Object> property : (Iterable<Map<String, Object>>) profile.get("properties")) {
if (property.get("name").equals("textures")) {
Map<String, Object> result = (Map<String, Object>) JsonParser
.parse(new String(Base64.decode((String) property.get("value"))));
result = (Map<String, Object>) result.get("textures");
return result;
}
}
return null; return null;
URLConnection connection = UrlUtils.openConnection(texture.getUrl(), proxy);
try (InputStream in = connection.getInputStream()) {
// thank you craftycodie!
// this is heavily based on
// https://github.com/Mojang/LegacyLauncher/pull/33/files#diff-b61023785a9260651ca0a223573ea9acb5be5eec478bff626dafb7abe13ffebaR99
BufferedImage image = ImageIO.read(in);
Graphics2D graphics = image.createGraphics();
graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
BufferedImage subimage;
if (image.getHeight() > 32) {
// flatten second layers
subimage = image.getSubimage(0, 32, 56, 16);
graphics.drawImage(subimage, 0, 16, null);
} }
if (texture.isSlim()) {
// convert slim to wide
subimage = image.getSubimage(45, 16, 9, 16);
graphics.drawImage(subimage, 46, 16, null);
subimage = image.getSubimage(49, 16, 2, 4);
graphics.drawImage(subimage, 50, 16, null);
subimage = image.getSubimage(53, 20, 2, 12);
graphics.drawImage(subimage, 54, 20, null);
}
graphics.dispose();
// crop the image
ByteArrayOutputStream out = new ByteArrayOutputStream();
image = image.getSubimage(0, 0, 64, 32);
ImageIO.write(image, "png", out);
return new CustomUrlConnection(out.toByteArray());
} }
} }

View File

@ -0,0 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.utils.api;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
import org.prismlauncher.utils.Base64;
import org.prismlauncher.utils.JsonParser;
/**
* Basic access to Mojang's Minecraft API.
*/
@SuppressWarnings("unchecked")
public final class MojangApi {
public static String getUuid(String username) throws IOException {
try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + username).openStream()) {
Map<String, Object> map = (Map<String, Object>) JsonParser.parse(in);
return (String) map.get("id");
}
}
public static Texture getTexture(String player, String name) throws IOException {
Map<String, Object> map = getTextures(player);
if (map != null) {
map = (Map<String, Object>) map.get(name);
if (map == null)
return null;
URL url = new URL((String) map.get("url"));
boolean slim = false;
if (name.equals("SKIN")) {
map = (Map<String, Object>) map.get("metadata");
if (map != null && "slim".equals(map.get("model")))
slim = true;
}
return new Texture(url, slim);
}
return null;
}
public static Map<String, Object> getTextures(String player) throws IOException {
try (InputStream profileIn = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + player)
.openStream()) {
Map<String, Object> profile = (Map<String, Object>) JsonParser.parse(profileIn);
for (Map<String, Object> property : (Iterable<Map<String, Object>>) profile.get("properties")) {
if (property.get("name").equals("textures")) {
Map<String, Object> result = (Map<String, Object>) JsonParser
.parse(new String(Base64.decode((String) property.get("value"))));
result = (Map<String, Object>) result.get("textures");
return result;
}
}
return null;
}
}
}

View File

@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.utils.api;
import java.net.URL;
/**
* Represents a texture from the Mojang API.
*/
public final class Texture {
private final URL url;
private final boolean slim;
public Texture(URL url, boolean slim) {
this.url = url;
this.slim = slim;
}
public URL getUrl() {
return url;
}
public boolean isSlim() {
return slim;
}
}

View File

@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Linking this library statically or dynamically with other modules is
* making a combined work based on this library. Thus, the terms and
* conditions of the GNU General Public License cover the whole
* combination.
*
* As a special exception, the copyright holders of this library give
* you permission to link this library with independent modules to
* produce an executable, regardless of the license terms of these
* independent modules, and to copy and distribute the resulting
* executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the
* license of that module. An independent module is a module which is
* not derived from or based on this library. If you modify this
* library, you may extend this exception to your version of the
* library, but you are not obliged to do so. If you do not wish to do
* so, delete this exception statement from your version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.prismlauncher.utils.url;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
public class CustomUrlConnection extends HttpURLConnection {
private InputStream in;
public CustomUrlConnection(byte[] data) {
this(new ByteArrayInputStream(data));
}
public CustomUrlConnection(InputStream in) {
super(null);
this.in = in;
}
@Override
public void connect() throws IOException {
responseCode = 200;
}
@Override
public void disconnect() {
try {
in.close();
} catch (IOException e) {
}
}
@Override
public InputStream getInputStream() throws IOException {
return in;
}
@Override
public boolean usingProxy() {
return false;
}
}

View File

@ -33,7 +33,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.prismlauncher.utils; package org.prismlauncher.utils.url;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
@ -81,11 +81,17 @@ public final class UrlUtils {
return http != null && openConnection != null; return http != null && openConnection != null;
} }
public static URLConnection openHttpConnection(URL url, Proxy proxy) throws IOException { public static URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (http == null) if (http == null)
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
if (url.getProtocol().equals("http"))
return openConnection(http, url, proxy); return openConnection(http, url, proxy);
// fall back to Java's default method
// at this point, this should not cause a StackOverflowError unless we've missed
// a protocol out from the if statements
return url.openConnection();
} }
public static URLConnection openConnection(URLStreamHandler handler, URL url, Proxy proxy) throws IOException { public static URLConnection openConnection(URLStreamHandler handler, URL url, Proxy proxy) throws IOException {