Merge branch 'evan-goode/fetch-missing-textures' into develop

This commit is contained in:
Evan Goode 2025-09-04 00:50:47 -04:00
commit 6d66308929
3 changed files with 228 additions and 8 deletions

View File

@ -61,6 +61,7 @@ import moe.yushi.authlibinjector.transform.support.BungeeCordProfileKeyTransform
import moe.yushi.authlibinjector.transform.support.CitizensTransformer;
import moe.yushi.authlibinjector.transform.support.ConcatenateURLTransformUnit;
import moe.yushi.authlibinjector.transform.support.ConstantURLTransformUnit;
import moe.yushi.authlibinjector.transform.support.FetchMissingTexturesByPlayerName;
import moe.yushi.authlibinjector.transform.support.MC52974Workaround;
import moe.yushi.authlibinjector.transform.support.MC52974_1710Workaround;
import moe.yushi.authlibinjector.transform.support.MainArgumentsTransformer;
@ -270,7 +271,7 @@ public final class AuthlibInjector {
return a.equals(b);
}
private static List<URLFilter> createFilters(APIMetadata config) {
private static List<URLFilter> createFilters(APIMetadata config, YggdrasilClient customClient, YggdrasilClient mojangClient) {
if (Config.httpdDisabled) {
log(INFO, "Disabled local HTTP server");
return emptyList();
@ -278,9 +279,6 @@ public final class AuthlibInjector {
List<URLFilter> filters = new ArrayList<>();
YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config));
YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider(), Config.mojangProxy);
boolean legacySkinPolyfillDefault = !Boolean.TRUE.equals(config.getMeta().get("feature.legacy_skin_api"));
if (Config.legacySkinPolyfill.isEnabled(legacySkinPolyfillDefault)) {
filters.add(new LegacySkinAPIFilter(customClient));
@ -312,7 +310,10 @@ public final class AuthlibInjector {
}
private static ClassTransformer createTransformer(APIMetadata config) {
URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config));
YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config));
YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider(), Config.mojangProxy);
URLProcessor urlProcessor = new URLProcessor(createFilters(config, customClient, mojangClient), new DefaultURLRedirector(config));
ClassTransformer transformer = new ClassTransformer();
transformer.setIgnores(Config.ignoredPackages);
@ -330,6 +331,9 @@ public final class AuthlibInjector {
transformer.units.add(new CitizensTransformer());
transformer.units.add(new ConcatenateURLTransformUnit());
FetchMissingTexturesByPlayerName.setYggdrasilClient(customClient);
transformer.units.add(new FetchMissingTexturesByPlayerName());
boolean usernameCheckDefault = Boolean.TRUE.equals(config.getMeta().get("feature.username_check"));
if (Config.usernameCheck.isEnabled(usernameCheckDefault)) {
log(INFO, "Username check is enforced");

View File

@ -137,7 +137,7 @@ public class ClassTransformer implements ClassFileTransformer {
public void accept(TransformUnit... units) {
long t0 = System.nanoTime();
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
TransformContextImpl[] ctxs = new TransformContextImpl[units.length];
ClassVisitor chain = writer;
@ -204,7 +204,7 @@ public class ClassTransformer implements ClassFileTransformer {
}
ClassReader reader = getClassReader();
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
@ -218,7 +218,7 @@ public class ClassTransformer implements ClassFileTransformer {
private void injectGeneratedMethods() {
ClassReader reader = getClassReader();
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

View File

@ -0,0 +1,216 @@
package moe.yushi.authlibinjector.transform.support;
import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
import static moe.yushi.authlibinjector.util.Logging.Level.ERROR;
import static moe.yushi.authlibinjector.util.Logging.Level.INFO;
import static moe.yushi.authlibinjector.util.Logging.log;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
import moe.yushi.authlibinjector.yggdrasil.GameProfile.PropertyValue;
import moe.yushi.authlibinjector.yggdrasil.GameProfile;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
public class FetchMissingTexturesByPlayerName implements TransformUnit {
private static volatile YggdrasilClient yggdrasilClient;
public static void setYggdrasilClient(YggdrasilClient yggdrasilClient) {
FetchMissingTexturesByPlayerName.yggdrasilClient = yggdrasilClient;
}
private static final ConcurrentHashMap<String, Optional<UUID>> nameToUUIDCache = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<UUID, Optional<PropertyValue>> uuidToTexturesCache = new ConcurrentHashMap<>();
public static Object getMissingTexturesProperty(Object profile) {
// (com.mojang.authlib.GameProfile) -> com.mojang.authlib.properties.Property
// Fetches missing textures for a GameProfile by player name via the yggdrasilClient.
try {
// If the GameProfile already has textures, return null.
Class<?> gameProfileClass = profile.getClass();
Method getProperties = gameProfileClass.getMethod("getProperties");
Object propertiesMap = getProperties.invoke(profile);
Method containsKey = propertiesMap.getClass().getMethod("containsKey", Object.class);
boolean hasTextures = (boolean) containsKey.invoke(propertiesMap, "textures");
if (hasTextures) {
return null;
}
Method getName = gameProfileClass.getMethod("getName");
String name = (String) getName.invoke(profile);
Optional<UUID> maybeUUID = nameToUUIDCache.computeIfAbsent(name, n -> {
try {
return yggdrasilClient.queryUUID(n);
} catch (UncheckedIOException e) {
return null;
}
});
if (maybeUUID == null || !maybeUUID.isPresent()) {
return null;
}
UUID uuid = maybeUUID.get();
Optional<PropertyValue> maybeTextures = uuidToTexturesCache.computeIfAbsent(uuid, u -> {
Optional<GameProfile> maybeFullProfile;
try {
maybeFullProfile = yggdrasilClient.queryProfile(u, true);
} catch (UncheckedIOException e) {
return null;
}
return maybeFullProfile.map(fullProfile -> {
return fullProfile.properties.get("textures");
});
});
if (maybeTextures == null || !maybeTextures.isPresent()) {
return null;
}
PropertyValue textures = maybeTextures.get();
log(INFO, "Successfully fetched missing textures for player " + name);
Class<?> propertyClass = profile.getClass()
.getClassLoader()
.loadClass("com.mojang.authlib.properties.Property");
Constructor<?> propertyConstructor = propertyClass.getConstructor(String.class, String.class, String.class);
return propertyConstructor.newInstance("textures", textures.value, textures.signature);
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
@CallbackMethod
public static Object getTextures(Object instance, Object profile, boolean requireSecure) {
try {
Object property = getMissingTexturesProperty(profile);
if (property != null) {
// Fill in the existing GameProfile with the missing textures
Class<?> gameProfileClass = profile.getClass();
Method getProperties = gameProfileClass.getMethod("getProperties");
Object propertiesMap = getProperties.invoke(profile);
Class<?> propertiesMapClass = propertiesMap.getClass();
Method removeAll = propertiesMapClass.getMethod("removeAll", Object.class);
removeAll.invoke(propertiesMap, "textures");
Method put = propertiesMapClass.getMethod("put", Object.class, Object.class);
put.invoke(propertiesMap, "textures", property);
}
Method m = instance.getClass().getDeclaredMethod("getTextures$original", profile.getClass(), boolean.class);
m.setAccessible(true);
return m.invoke(instance, profile, requireSecure);
} catch (Throwable e) {
log(ERROR, "Error fetching missing textures:");
e.printStackTrace();
return null;
}
}
@CallbackMethod
public static Object getPackedTextures(Object instance, Object profile) {
try {
Object property = getMissingTexturesProperty(profile);
if (property != null) {
return property;
}
Method m = instance.getClass().getDeclaredMethod("getPackedTextures$original", profile.getClass());
m.setAccessible(true);
return m.invoke(instance, profile);
} catch (Throwable e) {
log(ERROR, "Error fetching missing textures:");
e.printStackTrace();
return null;
}
}
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if ("getPackedTextures".equals(name) &&
"(Lcom/mojang/authlib/GameProfile;)Lcom/mojang/authlib/properties/Property;".equals(desc)) {
ctx.markModified();
MethodVisitor originalMethodVisitor = super.visitMethod(access, name + "$original", desc, signature, exceptions);
MethodVisitor hookedMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if (hookedMethodVisitor != null) {
hookedMethodVisitor.visitCode();
// Load `this`
hookedMethodVisitor.visitVarInsn(ALOAD, 0);
// Load `profile`
hookedMethodVisitor.visitVarInsn(ALOAD, 1);
hookedMethodVisitor.visitMethodInsn(INVOKESTATIC,
"moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName",
"getPackedTextures",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
false);
hookedMethodVisitor.visitTypeInsn(CHECKCAST, "com/mojang/authlib/properties/Property");
hookedMethodVisitor.visitInsn(ARETURN);
hookedMethodVisitor.visitMaxs(2, 2);
hookedMethodVisitor.visitEnd();
}
return originalMethodVisitor;
} else if ("getTextures".equals(name) &&
"(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;".equals(desc)) {
ctx.markModified();
MethodVisitor originalMethodVisitor = super.visitMethod(access, name + "$original", desc, signature, exceptions);
MethodVisitor hookedMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if (hookedMethodVisitor != null) {
hookedMethodVisitor.visitCode();
// Load `this`
hookedMethodVisitor.visitVarInsn(ALOAD, 0);
// Load `profile`
hookedMethodVisitor.visitVarInsn(ALOAD, 1);
// Load `requireSecure`
hookedMethodVisitor.visitVarInsn(ILOAD, 2);
hookedMethodVisitor.visitMethodInsn(INVOKESTATIC,
"moe/yushi/authlibinjector/transform/support/FetchMissingTexturesByPlayerName",
"getTextures",
"(Ljava/lang/Object;Ljava/lang/Object;Z)Ljava/lang/Object;",
false);
hookedMethodVisitor.visitTypeInsn(CHECKCAST, "java/util/Map");
hookedMethodVisitor.visitInsn(ARETURN);
hookedMethodVisitor.visitMaxs(3, 3);
hookedMethodVisitor.visitEnd();
}
return originalMethodVisitor;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
});
}
return Optional.empty();
}
@Override
public String toString() {
return "FetchMissingTexturesByPlayerName";
}
}