From d96bae7f4c8aac42fa0d80a81e7d833f564c7d1c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:28:53 -0400 Subject: [PATCH 1/9] Feat: Dynamic remapping --- .../lambda/command/commands/ReplayCommand.kt | 2 +- .../module/modules/player/MapDownloader.kt | 2 +- .../lambda/module/modules/player/Replay.kt | 2 +- .../kotlin/com/lambda/network/CapeManager.kt | 28 ++--- .../network/api/v1/endpoints/GetMappings.kt | 37 ++++++ .../util/DynamicReflectionSerializer.kt | 33 ++++- .../main/kotlin/com/lambda/util/FileUtils.kt | 117 ++++++++++++++++++ .../kotlin/com/lambda/util/FolderRegister.kt | 44 ------- .../kotlin/com/lambda/util/extension/Other.kt | 3 - 9 files changed, 200 insertions(+), 68 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt create mode 100644 common/src/main/kotlin/com/lambda/util/FileUtils.kt diff --git a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt index 4d079f4cd..dfad53b69 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -27,8 +27,8 @@ import com.lambda.brigadier.executeWithResult import com.lambda.brigadier.required import com.lambda.command.LambdaCommand import com.lambda.module.modules.player.Replay +import com.lambda.util.FileUtils.listRecursive import com.lambda.util.FolderRegister -import com.lambda.util.FolderRegister.listRecursive import com.lambda.util.extension.CommandBuilder import kotlin.io.path.exists diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt index dc44bf8f5..6184d4c64 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt @@ -21,8 +21,8 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.FileUtils.locationBoundDirectory import com.lambda.util.FolderRegister -import com.lambda.util.FolderRegister.locationBoundDirectory import com.lambda.util.StringUtils.hashString import com.lambda.util.player.SlotUtils.combined import com.lambda.util.world.entitySearch diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt index ab67218a5..371527e38 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -38,8 +38,8 @@ import com.lambda.sound.SoundManager.playSound import com.lambda.util.Communication.info import com.lambda.util.Communication.logError import com.lambda.util.Communication.warn +import com.lambda.util.FileUtils.locationBoundDirectory import com.lambda.util.FolderRegister -import com.lambda.util.FolderRegister.locationBoundDirectory import com.lambda.util.Formatting.asString import com.lambda.util.Formatting.getTime import com.lambda.util.KeyCode diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index 93873b3c4..508f8b8ed 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -19,6 +19,7 @@ package com.lambda.network import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.requests.CancellableRequest +import com.lambda.Lambda import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.core.Loadable @@ -31,11 +32,13 @@ import com.lambda.network.api.v1.models.Cape import com.lambda.sound.SoundManager.toIdentifier import com.lambda.util.Communication.info import com.lambda.util.Communication.logError +import com.lambda.util.FileUtils.downloadIfNotPresent import com.lambda.util.FolderRegister.capes import com.lambda.util.extension.get import com.lambda.util.extension.resolveFile import net.minecraft.client.texture.NativeImage.read import net.minecraft.client.texture.NativeImageBackedTexture +import java.io.File import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.io.path.ExperimentalPathApi @@ -73,21 +76,18 @@ object CapeManager : ConcurrentHashMap(), Loadable { failure = { logError("Could not fetch the cape of the player", it) } ) - private fun SafeContext.download(cape: Cape): CancellableRequest = - Fuel.download(cape.url) - .fileDestination { _, _ -> capes.resolveFile("${cape.id}.png") } - .response { result -> - result.fold( - success = { - val image = TextureUtils.readImage(it) - val native = NativeImageBackedTexture(image) - val id = cape.identifier + private fun SafeContext.download(cape: Cape): File = + capes.resolveFile("${cape.id}.png") + .downloadIfNotPresent(cape.url, + success = { + val image = TextureUtils.readImage(it) + val native = NativeImageBackedTexture(image) + val id = cape.identifier - mc.textureManager.registerTexture(id, native) - }, - failure = { logError("Could not download the cape", it) } - ) - } + Lambda.mc.textureManager.registerTexture(id, native) + }, + failure = { logError("Could not download the cape", it) }, + ) override fun load() = "Loaded ${images.size} cached capes" diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt new file mode 100644 index 000000000..304777d0b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Lambda + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.network.api.v1.endpoints + +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.core.requests.CancellableRequest +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion +import net.minecraft.SharedConstants + +/** + * Gets the Minecraft mappings for dynamic remapping + * + * Example: + * - version: 765 + * + * response: File or error + */ +fun getMappings(version: String = SharedConstants.VERSION_NAME, success: (String) -> Unit, failure: (FuelError) -> Unit): CancellableRequest = + Fuel.get("$apiUrl/api/${apiVersion.value}/mappings?version=$version") + .responseString { _, _, result -> result.fold(success, failure) } diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index 042280e6c..f5aea06ca 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -17,7 +17,15 @@ package com.lambda.util +import com.lambda.Lambda.LOG +import com.lambda.core.Loadable +import com.lambda.network.api.v1.endpoints.getMappings +import com.lambda.util.FileUtils.getIfNotPresent +import com.lambda.util.FileUtils.ifNotExists +import com.lambda.util.FolderRegister.cache +import com.lambda.util.extension.resolveFile import com.mojang.serialization.Codec +import net.minecraft.SharedConstants import net.minecraft.block.BlockState import net.minecraft.client.resource.language.TranslationStorage import net.minecraft.item.ItemStack @@ -34,7 +42,7 @@ import java.lang.reflect.Field import java.lang.reflect.InaccessibleObjectException import java.util.* -object DynamicReflectionSerializer { +object DynamicReflectionSerializer : Loadable { // Classes that should not be recursively serialized private val skipables = setOf( Codec::class.java, @@ -62,6 +70,19 @@ object DynamicReflectionSerializer { private const val INDENT = 2 + val mappings = cache.resolveFile("mappings-${SharedConstants.getProtocolVersion()}.m") + .ifNotExists { getMappings(success = it.getIfNotPresent(), failure = { LOG.error("Could not download the required files for the dynamic remapper") }).join() } + .let { file -> + file.readLines() + .map { it.split('\t') } + .associate { it[0] to it[1] } + } + + inline val Class.dynamicName: String get() = mappings.getOrDefault(simpleName, simpleName) + + inline val Field.dynamicName: String get() = mappings.getOrDefault(name, name) + + // ToDo: To make this work in production, every field could be remapped. fun Any.dynamicString( maxRecursionDepth: Int = 6, @@ -71,12 +92,12 @@ object DynamicReflectionSerializer { builder: StringBuilder = StringBuilder(), ): String { if (visitedObjects.contains(this)) { - builder.appendLine("$indent${javaClass.simpleName} (Circular Reference)") + builder.appendLine("$indent${javaClass.dynamicName} (Circular Reference)") return builder.toString() } visitedObjects.add(this) - builder.appendLine("$indent${javaClass.simpleName}") + builder.appendLine("$indent${javaClass.dynamicName}") val fields = javaClass.declaredFields + javaClass.superclass?.declaredFields.orEmpty() fields.forEach { field -> @@ -103,7 +124,7 @@ object DynamicReflectionSerializer { } val fieldValue = field.get(this) val fieldIndent = indent + " ".repeat(INDENT) - builder.appendLine("$fieldIndent${field.name}: ${fieldValue.formatFieldValue()}") + builder.appendLine("$fieldIndent${field.dynamicName}: ${fieldValue.formatFieldValue()}") if (currentDepth < maxRecursionDepth && fieldValue != null @@ -139,4 +160,8 @@ object DynamicReflectionSerializer { is RegistryEntry<*> -> "${value()}" else -> this?.toString() ?: "null" } + + override fun load(): String { + return "Loaded ${mappings.size} remapped named" + } } diff --git a/common/src/main/kotlin/com/lambda/util/FileUtils.kt b/common/src/main/kotlin/com/lambda/util/FileUtils.kt new file mode 100644 index 000000000..f50ef5c6b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/FileUtils.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2025 Lambda + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util + +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.httpDownload +import com.github.kittinunf.fuel.httpGet +import com.github.kittinunf.result.getOrNull +import com.lambda.Lambda.mc +import com.lambda.util.StringUtils.sanitizeForFilename +import java.io.File +import java.net.InetSocketAddress + +object FileUtils { + /** + * Returns a sequence of all the files in a tree that matches the [predicate] + */ + fun File.listRecursive(predicate: (File) -> Boolean): Sequence = walk().filter(predicate) + + /** + * Ensures the current file exists by creating it if it does not. + * + * If the file already exists, it will not be recreated. The necessary + * parent directories will be created if they do not exist. + */ + fun File.createIfNotExists(): File = also { parentFile.mkdirs(); createNewFile() } + + /** + * Retrieves or creates a directory based on the current network connection and world dimension. + * + * The directory is determined by the host name of the current network connection (or "singleplayer" if offline) + * and the dimension key of the current world. These values are sanitized for use as filenames and combined + * to form a path under the current file. If the directory does not exist, it will be created. + * + * @receiver The base directory where the location-bound directory will be created. + * @return A `File` object representing the location-bound directory. + * + * The path is structured as: + * - `[base directory]/[host name]/[dimension key]` + * + * Example: + * If playing on a server with hostname "example.com" and in the "overworld" dimension, the path would be: + * - `[base directory]/example.com/overworld` + */ + fun File.locationBoundDirectory(): File { + val hostName = (mc.networkHandler?.connection?.address as? InetSocketAddress)?.hostName ?: "singleplayer" + val path = resolve( + hostName.sanitizeForFilename() + ).resolve( + mc.world?.dimensionKey?.value?.path?.sanitizeForFilename() ?: "unknown" // TODO: Change with utils when merged to master + ) + path.mkdirs() + return path + } + + /** + * Executes the [block] if the receiver file exists + */ + inline fun File.ifExists(block: (File) -> Unit): File { + if (exists()) block(this) + return this + } + + /** + * Executes the [block] if the receiver file does not exist. + */ + inline fun File.ifNotExists(block: (File) -> Unit): File { + if (!exists()) block(this) + return this + } + + /** + * Downloads the given file url if the file is not present + * + * This function does not guarantee that the given file will be created + */ + fun File.downloadIfNotPresent(url: String, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File = + ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + + /** + * Downloads the given file url if the file is not present + * + * This function does not guarantee that the given file will be created + */ + fun String.downloadIfNotPresent(file: File, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File = + file.ifNotExists { httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + + /** + * Downloads the given file url if the file is not present + * + * This function does not guarantee that the given file will be created + */ + fun File.downloadIfNotPresent(): (String) -> Unit { + return { url -> ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, _ -> } } } } + + /** + * Gets the given url if the file is not present + * + * This function does not guarantee that the given file will be created + */ + fun File.getIfNotPresent(): (String) -> Unit { return { url -> ifNotExists { url.httpGet().responseString().third.getOrNull()?.let { writeText(it) } } } } +} diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 39044dff7..3e7f487c9 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -24,9 +24,6 @@ import com.lambda.util.FolderRegister.lambda import com.lambda.util.FolderRegister.minecraft import com.lambda.util.FolderRegister.packetLogs import com.lambda.util.FolderRegister.replay -import com.lambda.util.StringUtils.sanitizeForFilename -import java.io.File -import java.net.InetSocketAddress import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.notExists @@ -62,45 +59,4 @@ object FolderRegister : Loadable { "Created directories: ${createdFolders.joinToString { minecraft.parent.relativize(it).toString() }}" } else "Loaded ${folders.size} directories" } - - /** - * Ensures the current file exists by creating it if it does not. - * - * If the file already exists, it will not be recreated. The necessary - * parent directories will be created if they do not exist. - */ - fun File.createIfNotExists(): File = also { parentFile.mkdirs(); createNewFile() } - - /** - * Returns a sequence of all the files in a tree that matches the [predicate] - */ - fun File.listRecursive(predicate: (File) -> Boolean) = walk().filter(predicate) - - /** - * Retrieves or creates a directory based on the current network connection and world dimension. - * - * The directory is determined by the host name of the current network connection (or "singleplayer" if offline) - * and the dimension key of the current world. These values are sanitized for use as filenames and combined - * to form a path under the current file. If the directory does not exist, it will be created. - * - * @receiver The base directory where the location-bound directory will be created. - * @return A `File` object representing the location-bound directory. - * - * The path is structured as: - * - `[base directory]/[host name]/[dimension key]` - * - * Example: - * If playing on a server with hostname "example.com" and in the "overworld" dimension, the path would be: - * - `[base directory]/example.com/overworld` - */ - fun File.locationBoundDirectory(): File { - val hostName = (mc.networkHandler?.connection?.address as? InetSocketAddress)?.hostName ?: "singleplayer" - val path = resolve( - hostName.sanitizeForFilename() - ).resolve( - mc.world?.dimensionKey?.value?.path?.sanitizeForFilename() ?: "unknown" // TODO: Change with utils when merged to master - ) - path.mkdirs() - return path - } } diff --git a/common/src/main/kotlin/com/lambda/util/extension/Other.kt b/common/src/main/kotlin/com/lambda/util/extension/Other.kt index 979df28f7..8b2ce2012 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Other.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Other.kt @@ -23,9 +23,6 @@ import net.minecraft.client.texture.TextureManager import net.minecraft.util.Identifier import java.io.File import java.nio.file.Path -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract val GameProfile.isOffline get() = properties.isEmpty From 84352c06880f60988211fc647ee080392db3f1fb Mon Sep 17 00:00:00 2001 From: Constructor Date: Wed, 19 Mar 2025 02:49:13 +0100 Subject: [PATCH 2/9] Fix inner classes not being remapped and cleaned up --- .../module/modules/network/PacketLogger.kt | 10 ++-- .../network/api/v1/endpoints/GetMappings.kt | 7 ++- .../util/DynamicReflectionSerializer.kt | 52 +++++++++++-------- .../main/kotlin/com/lambda/util/FileUtils.kt | 23 +++++--- .../kotlin/com/lambda/util/FolderRegister.kt | 3 ++ 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt index c36c45880..c33dfc85a 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt @@ -30,6 +30,7 @@ import com.lambda.util.Communication import com.lambda.util.Communication.info import com.lambda.util.DynamicReflectionSerializer.dynamicString import com.lambda.util.FolderRegister +import com.lambda.util.FolderRegister.relativeMCPath import com.lambda.util.Formatting.getTime import com.lambda.util.text.* import kotlinx.coroutines.channels.BufferOverflow @@ -37,7 +38,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import net.minecraft.network.packet.Packet import java.awt.Color import java.io.File -import java.nio.file.Path import java.time.format.DateTimeFormatter import kotlin.io.path.pathString @@ -83,7 +83,6 @@ object PacketLogger : Module( extraBufferCapacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - private val File.relativePath: Path get() = mc.runDirectory.toPath().relativize(toPath()) init { runIO { @@ -105,7 +104,7 @@ object PacketLogger : Module( createNewFile() } val info = buildText { - clickEvent(ClickEvents.openFile(relativePath.pathString)) { + clickEvent(ClickEvents.openFile(relativeMCPath.pathString)) { literal("Packet logger started: ") color(Color.YELLOW) { literal(fileName) } literal(" (click to open)") @@ -113,7 +112,6 @@ object PacketLogger : Module( } this@PacketLogger.info(info) }.apply { - // ToDo: Add more rich and accurate data to the header StringBuilder().apply { appendLine(Communication.ascii) appendLine("${Lambda.SYMBOL} - Lambda ${Lambda.VERSION} - Packet Log") @@ -144,8 +142,8 @@ object PacketLogger : Module( file?.let { val info = buildText { literal("Stopped logging packets to ") - clickEvent(ClickEvents.openFile(it.relativePath.pathString)) { - color(Color.YELLOW) { literal(it.relativePath.pathString) } + clickEvent(ClickEvents.openFile(it.relativeMCPath.pathString)) { + color(Color.YELLOW) { literal(it.relativeMCPath.pathString) } literal(" (click to open)") } } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt index 304777d0b..c12aa1cbe 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt @@ -32,6 +32,9 @@ import net.minecraft.SharedConstants * * response: File or error */ -fun getMappings(version: String = SharedConstants.VERSION_NAME, success: (String) -> Unit, failure: (FuelError) -> Unit): CancellableRequest = - Fuel.get("$apiUrl/api/${apiVersion.value}/mappings?version=$version") +fun getMappings( + version: String = SharedConstants.getGameVersion().name, + success: (String) -> Unit, + failure: (FuelError) -> Unit +) = Fuel.get("$apiUrl/api/${apiVersion.value}/mappings?version=$version") .responseString { _, _, result -> result.fold(success, failure) } diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index f5aea06ca..e54830023 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -17,6 +17,7 @@ package com.lambda.util +import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.core.Loadable import com.lambda.network.api.v1.endpoints.getMappings @@ -70,38 +71,45 @@ object DynamicReflectionSerializer : Loadable { private const val INDENT = 2 - val mappings = cache.resolveFile("mappings-${SharedConstants.getProtocolVersion()}.m") - .ifNotExists { getMappings(success = it.getIfNotPresent(), failure = { LOG.error("Could not download the required files for the dynamic remapper") }).join() } - .let { file -> - file.readLines() - .map { it.split('\t') } - .associate { it[0] to it[1] } - } - - inline val Class.dynamicName: String get() = mappings.getOrDefault(simpleName, simpleName) - - inline val Field.dynamicName: String get() = mappings.getOrDefault(name, name) + private val mappings = + cache.resolveFile("${SharedConstants.getProtocolVersion()}.mappings") + .ifNotExists { + getMappings( + success = it.getIfNotPresent(), + failure = { LOG.error("Could not download the required files for the dynamic remapper") } + ).join() + }.let { file -> + file.readLines() + .map { it.split('\t') } + .associate { it[0].split('$').last() to it[1] } + } + + private val String.remappedName get() = mappings.getOrDefault(this, this) + + private fun Class.dynamicName(remap: Boolean) = + if (remap) simpleName.remappedName else simpleName + private fun Field.dynamicName(remap: Boolean) = + if (remap) name.remappedName else name - - // ToDo: To make this work in production, every field could be remapped. fun Any.dynamicString( maxRecursionDepth: Int = 6, currentDepth: Int = 0, indent: String = "", visitedObjects: MutableSet = HashSet(), builder: StringBuilder = StringBuilder(), + remap: Boolean = !Lambda.isDebug, ): String { if (visitedObjects.contains(this)) { - builder.appendLine("$indent${javaClass.dynamicName} (Circular Reference)") + builder.appendLine("$indent${javaClass.dynamicName(remap)} (Circular Reference)") return builder.toString() } visitedObjects.add(this) - builder.appendLine("$indent${javaClass.dynamicName}") + builder.appendLine("$indent${javaClass.dynamicName(remap)}") val fields = javaClass.declaredFields + javaClass.superclass?.declaredFields.orEmpty() fields.forEach { field -> - processField(field, indent, builder, currentDepth, maxRecursionDepth, visitedObjects) + processField(field, indent, builder, currentDepth, maxRecursionDepth, visitedObjects, remap) } return builder.toString() @@ -114,6 +122,7 @@ object DynamicReflectionSerializer : Loadable { currentDepth: Int, maxRecursionDepth: Int, visitedObjects: MutableSet, + remap: Boolean, ) { if (skipFields.any { it.isAssignableFrom(field.type) }) return @@ -123,8 +132,8 @@ object DynamicReflectionSerializer : Loadable { return } val fieldValue = field.get(this) - val fieldIndent = indent + " ".repeat(INDENT) - builder.appendLine("$fieldIndent${field.dynamicName}: ${fieldValue.formatFieldValue()}") + val fieldIndent = "$indent${" ".repeat(INDENT)}" + builder.appendLine("$fieldIndent${field.dynamicName(remap)}: ${fieldValue.formatFieldValue()}") if (currentDepth < maxRecursionDepth && fieldValue != null @@ -136,9 +145,10 @@ object DynamicReflectionSerializer : Loadable { fieldValue.dynamicString( maxRecursionDepth, currentDepth + 1, - fieldIndent + " ".repeat(INDENT), + "$fieldIndent${" ".repeat(INDENT)}", visitedObjects, builder, + remap ) } } @@ -161,7 +171,5 @@ object DynamicReflectionSerializer : Loadable { else -> this?.toString() ?: "null" } - override fun load(): String { - return "Loaded ${mappings.size} remapped named" - } + override fun load() = "Loaded ${mappings.size} deobfuscated qualifier" } diff --git a/common/src/main/kotlin/com/lambda/util/FileUtils.kt b/common/src/main/kotlin/com/lambda/util/FileUtils.kt index f50ef5c6b..137be7791 100644 --- a/common/src/main/kotlin/com/lambda/util/FileUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/FileUtils.kt @@ -38,7 +38,7 @@ object FileUtils { * If the file already exists, it will not be recreated. The necessary * parent directories will be created if they do not exist. */ - fun File.createIfNotExists(): File = also { parentFile.mkdirs(); createNewFile() } + fun File.createIfNotExists() = also { parentFile.mkdirs(); createNewFile() } /** * Retrieves or creates a directory based on the current network connection and world dimension. @@ -89,29 +89,36 @@ object FileUtils { * * This function does not guarantee that the given file will be created */ - fun File.downloadIfNotPresent(url: String, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File = - ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + fun File.downloadIfNotPresent( + url: String, + success: (ByteArray) -> Unit = {}, + failure: (FuelError) -> Unit = {} + ) = ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } /** * Downloads the given file url if the file is not present * * This function does not guarantee that the given file will be created */ - fun String.downloadIfNotPresent(file: File, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File = - file.ifNotExists { httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + fun String.downloadIfNotPresent( + file: File, + success: (ByteArray) -> Unit = {}, + failure: (FuelError) -> Unit = {} + ) = file.ifNotExists { httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } /** * Downloads the given file url if the file is not present * * This function does not guarantee that the given file will be created */ - fun File.downloadIfNotPresent(): (String) -> Unit { - return { url -> ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, _ -> } } } } + fun File.downloadIfNotPresent(): (String) -> Unit = + { url -> ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, _ -> } } } /** * Gets the given url if the file is not present * * This function does not guarantee that the given file will be created */ - fun File.getIfNotPresent(): (String) -> Unit { return { url -> ifNotExists { url.httpGet().responseString().third.getOrNull()?.let { writeText(it) } } } } + fun File.getIfNotPresent(): (String) -> Unit = + { url -> ifNotExists { url.httpGet().responseString().third.getOrNull()?.let { writeText(it) } } } } diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 3e7f487c9..a61d03ee6 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -24,6 +24,7 @@ import com.lambda.util.FolderRegister.lambda import com.lambda.util.FolderRegister.minecraft import com.lambda.util.FolderRegister.packetLogs import com.lambda.util.FolderRegister.replay +import java.io.File import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.notExists @@ -48,6 +49,8 @@ object FolderRegister : Loadable { val structure: Path = lambda.resolve("structure") val maps: Path = lambda.resolve("maps") + val File.relativeMCPath: Path get() = minecraft.relativize(toPath()) + override fun load(): String { val folders = listOf(lambda, config, packetLogs, replay, cache, capes, structure, maps) val createdFolders = folders.mapNotNull { From 48da4f7a597e81302222cb1d0ddcd8fb598f413c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:03:13 -0400 Subject: [PATCH 3/9] Better field format mapping --- .../util/DynamicReflectionSerializer.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index e54830023..d783ba69e 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -84,11 +84,11 @@ object DynamicReflectionSerializer : Loadable { .associate { it[0].split('$').last() to it[1] } } - private val String.remappedName get() = mappings.getOrDefault(this, this) + val String.remappedName get() = mappings.getOrDefault(this, this) - private fun Class.dynamicName(remap: Boolean) = - if (remap) simpleName.remappedName else simpleName - private fun Field.dynamicName(remap: Boolean) = + fun Class.dynamicName(remap: Boolean) = + if (remap) canonicalName.remappedName else simpleName + fun Field.dynamicName(remap: Boolean) = if (remap) name.remappedName else name fun Any.dynamicString( @@ -133,7 +133,7 @@ object DynamicReflectionSerializer : Loadable { } val fieldValue = field.get(this) val fieldIndent = "$indent${" ".repeat(INDENT)}" - builder.appendLine("$fieldIndent${field.dynamicName(remap)}: ${fieldValue.formatFieldValue()}") + builder.appendLine("$fieldIndent${field.dynamicName(remap)}: ${fieldValue.formatFieldValue(remap)}") if (currentDepth < maxRecursionDepth && fieldValue != null @@ -153,14 +153,14 @@ object DynamicReflectionSerializer : Loadable { } } - private fun Any?.formatFieldValue(): String = + private fun Any?.formatFieldValue(remap: Boolean): String = when (this) { is String -> "\"${this}\"" - is Collection<*> -> "[${joinToString(", ") { it.formatFieldValue() }}]" - is Array<*> -> "[${joinToString(", ") { it.formatFieldValue() }}]" + is Collection<*> -> "[${joinToString(", ") { it.formatFieldValue(remap) }}]" + is Array<*> -> "[${joinToString(", ") { it.formatFieldValue(remap) }}]" is Map<*, *> -> "{${ entries.joinToString(", ") { (k, v) -> - "${k.formatFieldValue()}: ${v.formatFieldValue()}" + "${k.formatFieldValue(remap)}: ${v.formatFieldValue(remap)}" } }}" @@ -168,7 +168,11 @@ object DynamicReflectionSerializer : Loadable { is Identifier -> "$namespace:$path" is NbtCompound -> asString() is RegistryEntry<*> -> "${value()}" - else -> this?.toString() ?: "null" + else -> { + if (this?.javaClass?.canonicalName?.contains("minecraft") == true) + "${this.javaClass.dynamicName(remap).substringAfterLast('.')}@${Integer.toHexString(hashCode())}" + else this?.toString() ?: "null" + } } override fun load() = "Loaded ${mappings.size} deobfuscated qualifier" From b98738e000ff8b3ec9b11fb202a5d56e1f10bed3 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:03:29 -0400 Subject: [PATCH 4/9] Crash report dynamic mapping --- .../com/lambda/mixin/CrashReportMixin.java | 94 +++++++++++++++++++ .../com/lambda/util/DynamicException.kt | 56 +++++++++++ .../main/resources/lambda.mixins.common.json | 5 +- 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/CrashReportMixin.java create mode 100644 common/src/main/kotlin/com/lambda/util/DynamicException.kt diff --git a/common/src/main/java/com/lambda/mixin/CrashReportMixin.java b/common/src/main/java/com/lambda/mixin/CrashReportMixin.java new file mode 100644 index 000000000..23662f1b7 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/CrashReportMixin.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 Lambda + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin; + +import com.lambda.Lambda; +import com.lambda.util.DynamicException; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.Util; +import net.minecraft.util.crash.CrashReport; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +// Modify the crash report behavior for dynamic remapping and Easter egg +@Mixin(CrashReport.class) +public class CrashReportMixin { + @Mutable + @Shadow @Final private Throwable cause; + + @Inject(method = "(Ljava/lang/String;Ljava/lang/Throwable;)V", at = @At("TAIL")) + void injectConstructor(String message, Throwable cause, CallbackInfo ci) { + if (!Lambda.INSTANCE.isDebug() && MinecraftClient.getInstance() != null) { + this.cause = new DynamicException(cause); + } + } + + @Inject(method = "generateWittyComment()Ljava/lang/String;", at = @At("HEAD"), cancellable = true) + private static void generateWittyComment(CallbackInfoReturnable cir) { + String[] strings = new String[]{ + "Who set us up the TNT?", + "Everything's going to plan. No, really, that was supposed to happen.", + "Uh... Did I do that?", + "Oops.", + "Why did you do that?", + "I feel sad now :(", + "My bad.", + "I'm sorry, Dave.", + "I let you down. Sorry :(", + "On the bright side, I bought you a teddy bear!", + "Daisy, daisy...", + "Oh - I know what I did wrong!", + "Hey, that tickles! Hehehe!", + "I blame Dinnerbone.", + "You should try our sister game, Minceraft!", + "Don't be sad. I'll do better next time, I promise!", + "Don't be sad, have a hug! <3", + "I just don't know what went wrong :(", + "Shall we play a game?", + "Quite honestly, I wouldn't worry myself about that.", + "I bet Cylons wouldn't have this problem.", + "Sorry :(", + "Surprise! Haha. Well, this is awkward.", + "Would you like a cupcake?", + "Hi. I'm Minecraft, and I'm a crashaholic.", + "Ooh. Shiny.", + "This doesn't make any sense!", + "Why is it breaking :(", + "Don't do that.", + "Ouch. That hurt :(", + "You're mean.", + "This is a token for 1 free hug. Redeem at your nearest Mojangsta: [~~HUG~~]", + "There are four lights!", + "But it works on my machine.", + "Popbob was here.", + "The oldest anarchy server in Minecraft." + }; + + try { + cir.setReturnValue(strings[(int)(Util.getMeasuringTimeNano() % (long)strings.length)]); + } catch (Throwable var2) { + cir.setReturnValue("Witty comment unavailable :("); + } + } +} diff --git a/common/src/main/kotlin/com/lambda/util/DynamicException.kt b/common/src/main/kotlin/com/lambda/util/DynamicException.kt new file mode 100644 index 000000000..8187be232 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/DynamicException.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 Lambda + * + * 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, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util + +import com.lambda.util.DynamicReflectionSerializer.remappedName +import java.io.PrintStream +import java.io.PrintWriter + +class DynamicException(original: Throwable) : Throwable(original) { + private val remappedStackTrace = original.stackTrace.remapClassNames() + + init { + stackTrace = remappedStackTrace + } + + private fun Array.remapClassNames() = + map { element -> + StackTraceElement( + element.className.remappedName, + element.methodName.remappedName, + element.fileName, + element.lineNumber + ) + }.toTypedArray() + + override fun printStackTrace(s: PrintStream) { + s.println(this) + remappedStackTrace.forEach { element -> + s.println("\tat $element") + } + } + + override fun printStackTrace(s: PrintWriter) { + s.println(this) + remappedStackTrace.forEach { element -> + s.println("\tat $element") + } + } + + override fun toString(): String = localizedMessage +} diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 309b2f670..c11f98333 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -29,8 +29,8 @@ "render.BackgroundRendererMixin", "render.BlockRenderManagerMixin", "render.CameraMixin", - "render.ChatHudMixin", "render.CapeFeatureRendererMixin", + "render.ChatHudMixin", "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", @@ -55,7 +55,8 @@ "world.ClientChunkManagerMixin", "world.ClientWorldMixin", "world.StructureTemplateMixin", - "world.WorldMixin" + "world.WorldMixin", + "CrashReportMixin" ], "injectors": { "defaultRequire": 1 From 31d338d6c3f036604a65bf743ed2f64c1ce5c881 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:11:16 -0400 Subject: [PATCH 5/9] Added enabled module list --- .../com/lambda/mixin/CrashReportMixin.java | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/CrashReportMixin.java b/common/src/main/java/com/lambda/mixin/CrashReportMixin.java index 23662f1b7..7438db47e 100644 --- a/common/src/main/java/com/lambda/mixin/CrashReportMixin.java +++ b/common/src/main/java/com/lambda/mixin/CrashReportMixin.java @@ -18,6 +18,8 @@ package com.lambda.mixin; import com.lambda.Lambda; +import com.lambda.module.Module; +import com.lambda.module.ModuleRegistry; import com.lambda.util.DynamicException; import net.minecraft.client.MinecraftClient; import net.minecraft.util.Util; @@ -28,10 +30,11 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -// Modify the crash report behavior for dynamic remapping and Easter egg +// Modify the crash report behavior for dynamic remapping, Easter egg and github issue link @Mixin(CrashReport.class) public class CrashReportMixin { @Mutable @@ -44,46 +47,28 @@ void injectConstructor(String message, Throwable cause, CallbackInfo ci) { } } + @Redirect(method = "asString()Ljava/lang/String;", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/crash/CrashReport;addStackTrace(Ljava/lang/StringBuilder;)V")) + void injectString(CrashReport instance, StringBuilder stringBuilder) { + stringBuilder.append("If this issue is related to Lambda, check if other users have experienced this too, or create a new issue at https://github.com/lambda-client/lambda/issues.\n\n"); + + if (MinecraftClient.getInstance() != null) { + stringBuilder.append("Enabled modules:\n"); + + ModuleRegistry.INSTANCE.getModules() + .stream().filter(Module::isEnabled) + .forEach(m -> stringBuilder.append("\t").append(m.getName()).append("\n")); + } + + stringBuilder.append("\n"); + stringBuilder.append("-".repeat(43)); + stringBuilder.append("\n\n"); + + instance.addStackTrace(stringBuilder); + } + @Inject(method = "generateWittyComment()Ljava/lang/String;", at = @At("HEAD"), cancellable = true) private static void generateWittyComment(CallbackInfoReturnable cir) { - String[] strings = new String[]{ - "Who set us up the TNT?", - "Everything's going to plan. No, really, that was supposed to happen.", - "Uh... Did I do that?", - "Oops.", - "Why did you do that?", - "I feel sad now :(", - "My bad.", - "I'm sorry, Dave.", - "I let you down. Sorry :(", - "On the bright side, I bought you a teddy bear!", - "Daisy, daisy...", - "Oh - I know what I did wrong!", - "Hey, that tickles! Hehehe!", - "I blame Dinnerbone.", - "You should try our sister game, Minceraft!", - "Don't be sad. I'll do better next time, I promise!", - "Don't be sad, have a hug! <3", - "I just don't know what went wrong :(", - "Shall we play a game?", - "Quite honestly, I wouldn't worry myself about that.", - "I bet Cylons wouldn't have this problem.", - "Sorry :(", - "Surprise! Haha. Well, this is awkward.", - "Would you like a cupcake?", - "Hi. I'm Minecraft, and I'm a crashaholic.", - "Ooh. Shiny.", - "This doesn't make any sense!", - "Why is it breaking :(", - "Don't do that.", - "Ouch. That hurt :(", - "You're mean.", - "This is a token for 1 free hug. Redeem at your nearest Mojangsta: [~~HUG~~]", - "There are four lights!", - "But it works on my machine.", - "Popbob was here.", - "The oldest anarchy server in Minecraft." - }; + String[] strings = new String[]{"Who set us up the TNT?", "Everything's going to plan. No, really, that was supposed to happen.", "Uh... Did I do that?", "Oops.", "Why did you do that?", "I feel sad now :(", "My bad.", "I'm sorry, Dave.", "I let you down. Sorry :(", "On the bright side, I bought you a teddy bear!", "Daisy, daisy...", "Oh - I know what I did wrong!", "Hey, that tickles! Hehehe!", "I blame Dinnerbone.", "You should try our sister game, Minceraft!", "Don't be sad. I'll do better next time, I promise!", "Don't be sad, have a hug! <3", "I just don't know what went wrong :(", "Shall we play a game?", "Quite honestly, I wouldn't worry myself about that.", "I bet Cylons wouldn't have this problem.", "Sorry :(", "Surprise! Haha. Well, this is awkward.", "Would you like a cupcake?", "Hi. I'm Minecraft, and I'm a crashaholic.", "Ooh. Shiny.", "This doesn't make any sense!", "Why is it breaking :(", "Don't do that.", "Ouch. That hurt :(", "You're mean.", "This is a token for 1 free hug. Redeem at your nearest Mojangsta: [~~HUG~~]", "There are four lights!", "But it works on my machine.", "Popbob was here.", "The oldest anarchy server in Minecraft.", "Better luck next time..", "Fatal error occurred user is too based.", "Running premium software on a potato is not advised", "I don't know, ask that kilab guy", "Ah shit, here we go again.", "I will uhh, fix that sometime.", "Not a bug, a feature!", "You should try out Lambda on Windows XP.", "Blade did that."}; try { cir.setReturnValue(strings[(int)(Util.getMeasuringTimeNano() % (long)strings.length)]); From 08c07cfea22bf6bed3147c8627fb0ebfc1728dfa Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:33:23 -0400 Subject: [PATCH 6/9] Update DynamicReflectionSerializer.kt --- .../com/lambda/util/DynamicReflectionSerializer.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index d783ba69e..07254e615 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -79,6 +79,7 @@ object DynamicReflectionSerializer : Loadable { failure = { LOG.error("Could not download the required files for the dynamic remapper") } ).join() }.let { file -> + if (!file.exists()) emptyMap() file.readLines() .map { it.split('\t') } .associate { it[0].split('$').last() to it[1] } @@ -138,9 +139,9 @@ object DynamicReflectionSerializer : Loadable { if (currentDepth < maxRecursionDepth && fieldValue != null && !field.type.isPrimitive - && !field.type.isArray && - !field.type.isEnum && - skipables.none { it.isAssignableFrom(field.type) } + && !field.type.isArray + && !field.type.isEnum + && skipables.none { it.isAssignableFrom(field.type) } ) { fieldValue.dynamicString( maxRecursionDepth, @@ -170,7 +171,7 @@ object DynamicReflectionSerializer : Loadable { is RegistryEntry<*> -> "${value()}" else -> { if (this?.javaClass?.canonicalName?.contains("minecraft") == true) - "${this.javaClass.dynamicName(remap).substringAfterLast('.')}@${Integer.toHexString(hashCode())}" + "${this.javaClass.dynamicName(remap)}@${Integer.toHexString(hashCode())}" else this?.toString() ?: "null" } } From 842f68de1dbb08726c96423a7f0456b98d53ed5d Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:06:43 -0400 Subject: [PATCH 7/9] Better file utils --- .../lambda/module/modules/client/Network.kt | 4 + .../kotlin/com/lambda/network/LambdaHttp.kt | 13 +- .../network/api/v1/endpoints/GetCape.kt | 2 +- .../network/api/v1/endpoints/GetMappings.kt | 40 ----- .../network/api/v1/endpoints/LinkDiscord.kt | 2 +- .../lambda/network/api/v1/endpoints/Login.kt | 2 +- .../network/api/v1/endpoints/SetCape.kt | 2 +- .../util/DynamicReflectionSerializer.kt | 27 ++-- .../main/kotlin/com/lambda/util/FileUtils.kt | 148 ++++++++++++++---- 9 files changed, 144 insertions(+), 96 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt index 74ac5917f..833ed1694 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt @@ -31,6 +31,7 @@ import com.lambda.network.NetworkManager.updateToken import com.lambda.network.api.v1.endpoints.login import com.lambda.util.StringUtils.hash import com.lambda.util.extension.isOffline +import net.minecraft.SharedConstants import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler import net.minecraft.client.network.ServerAddress @@ -51,6 +52,9 @@ object Network : Module( val authServer by setting("Auth Server", "auth.lambda-client.org") val apiUrl by setting("API Server", "https://api.lambda-client.org") val apiVersion by setting("API Version", ApiVersion.V1) + val mappings by setting("Mappings", "https://mappings.lambda-client.org") + + val gameVersion = SharedConstants.getGameVersion().name private var hash: String? = null diff --git a/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt b/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt index 743f4240d..224a75624 100644 --- a/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt +++ b/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt @@ -34,11 +34,16 @@ val LambdaHttp = HttpClient { } } -suspend inline fun HttpClient.download(url: String, file: File, block: HttpRequestBuilder.() -> Unit = {}) = - file.writeBytes(get(url, block).readRawBytes()) +suspend inline fun HttpClient.download(url: String, file: File, block: HttpRequestBuilder.() -> Unit = {}) { + val response = get(url, block).readRawBytes() + file.writeBytes(response) +} -suspend inline fun HttpClient.download(url: String, output: OutputStream, block: HttpRequestBuilder.() -> Unit = {}) = - output.write(get(url, block).readRawBytes()) +suspend inline fun HttpClient.download(url: String, output: OutputStream, block: HttpRequestBuilder.() -> Unit = {}) { + val response = get(url, block).readRawBytes() + output.write(response) +} suspend inline fun HttpClient.download(url: String, block: HttpRequestBuilder.() -> Unit) = get(url, block).readRawBytes() + diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt index f1f710b57..dc11d8eb0 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt @@ -31,7 +31,7 @@ import java.util.UUID * Example: * - id: ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 * - * response: [Cape] or error + * @return results of cape */ suspend fun getCape(uuid: UUID) = runCatching { LambdaHttp.get("$apiUrl/api/${apiVersion.value}/cape?id=$uuid").body() diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt deleted file mode 100644 index c12aa1cbe..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetMappings.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * 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, either version 3 of the License, or - * (at your option) any later version. - * - * 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.network.api.v1.endpoints - -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.FuelError -import com.github.kittinunf.fuel.core.requests.CancellableRequest -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import net.minecraft.SharedConstants - -/** - * Gets the Minecraft mappings for dynamic remapping - * - * Example: - * - version: 765 - * - * response: File or error - */ -fun getMappings( - version: String = SharedConstants.getGameVersion().name, - success: (String) -> Unit, - failure: (FuelError) -> Unit -) = Fuel.get("$apiUrl/api/${apiVersion.value}/mappings?version=$version") - .responseString { _, _, result -> result.fold(success, failure) } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt index 9808719fd..50e5e6148 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt @@ -32,7 +32,7 @@ import io.ktor.http.* * Example: * - token: OTk1MTU1NzcyMzYxMTQ2NDM4 * - * response: [Authentication] or error + * @return result of [Authentication] */ suspend fun linkDiscord(discordToken: String) = runCatching { LambdaHttp.post("${apiUrl}/api/${apiVersion.value}/link/discord") { diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt index 60987a780..2a065dabb 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt @@ -32,7 +32,7 @@ import io.ktor.http.* * - username: Notch * - hash: 069a79f444e94726a5befca90e38aaf5 * - * response: [Authentication] or error + * @return result of [Authentication] */ suspend fun login(username: String, hash: String) = runCatching { LambdaHttp.post("${apiUrl}/api/${apiVersion.value}/login") { diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt index 1fec095ae..dee8d57e3 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt @@ -30,7 +30,7 @@ import io.ktor.http.* * Example: * - id: galaxy * - * response: [Unit] or error + * @return nothing */ suspend fun setCape(id: String) = runCatching { val resp = LambdaHttp.put("$apiUrl/api/${apiVersion.value}/cape?id=$id") { diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index 07254e615..d7b5ffc87 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -20,13 +20,12 @@ package com.lambda.util import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.core.Loadable -import com.lambda.network.api.v1.endpoints.getMappings -import com.lambda.util.FileUtils.getIfNotPresent -import com.lambda.util.FileUtils.ifNotExists +import com.lambda.module.modules.client.Network +import com.lambda.util.FileUtils.downloadIfNotPresent import com.lambda.util.FolderRegister.cache import com.lambda.util.extension.resolveFile import com.mojang.serialization.Codec -import net.minecraft.SharedConstants +import kotlinx.coroutines.runBlocking import net.minecraft.block.BlockState import net.minecraft.client.resource.language.TranslationStorage import net.minecraft.item.ItemStack @@ -71,19 +70,19 @@ object DynamicReflectionSerializer : Loadable { private const val INDENT = 2 - private val mappings = - cache.resolveFile("${SharedConstants.getProtocolVersion()}.mappings") - .ifNotExists { - getMappings( - success = it.getIfNotPresent(), - failure = { LOG.error("Could not download the required files for the dynamic remapper") } - ).join() - }.let { file -> - if (!file.exists()) emptyMap() + private val mappings = runBlocking { + "${Network.mappings}/${Network.gameVersion}" + .downloadIfNotPresent(cache.resolveFile(Network.gameVersion)) + .map { file -> file.readLines() - .map { it.split('\t') } + .map { it.split(' ') } .associate { it[0].split('$').last() to it[1] } } + .getOrElse { + LOG.error("Unable to download deobfuscated qualifiers", it) + emptyMap() + } + } val String.remappedName get() = mappings.getOrDefault(this, this) diff --git a/common/src/main/kotlin/com/lambda/util/FileUtils.kt b/common/src/main/kotlin/com/lambda/util/FileUtils.kt index 137be7791..ff493947f 100644 --- a/common/src/main/kotlin/com/lambda/util/FileUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/FileUtils.kt @@ -17,14 +17,15 @@ package com.lambda.util -import com.github.kittinunf.fuel.core.FuelError -import com.github.kittinunf.fuel.httpDownload -import com.github.kittinunf.fuel.httpGet -import com.github.kittinunf.result.getOrNull import com.lambda.Lambda.mc +import com.lambda.network.LambdaHttp +import com.lambda.network.download import com.lambda.util.StringUtils.sanitizeForFilename +import io.ktor.client.request.* import java.io.File import java.net.InetSocketAddress +import kotlin.math.sign +import kotlin.time.Duration object FileUtils { /** @@ -32,14 +33,6 @@ object FileUtils { */ fun File.listRecursive(predicate: (File) -> Boolean): Sequence = walk().filter(predicate) - /** - * Ensures the current file exists by creating it if it does not. - * - * If the file already exists, it will not be recreated. The necessary - * parent directories will be created if they do not exist. - */ - fun File.createIfNotExists() = also { parentFile.mkdirs(); createNewFile() } - /** * Retrieves or creates a directory based on the current network connection and world dimension. * @@ -69,56 +62,143 @@ object FileUtils { } /** - * Executes the [block] if the receiver file exists + * Executes the [block] if the file is older than the given [duration] + */ + fun File.isOlderThan(duration: Duration, block: (File) -> Unit) = + ifExists { if (duration.inWholeMilliseconds < System.currentTimeMillis() - lastModified()) block(this) } + + /** + * Returns whether the receiver file is older than [duration] + */ + fun File.isOlderThan(duration: Duration) = + duration.inWholeMilliseconds < System.currentTimeMillis() - lastModified() + + /** + * Executes the [block] if the receiver file exists and is not empty */ inline fun File.ifExists(block: (File) -> Unit): File { - if (exists()) block(this) + if (length() > 0) block(this) + return this + } + + /** + * Ensures the current file exists by creating it if it does not. + * + * If the file already exists, it will not be recreated. The necessary + * parent directories will be created if they do not exist. + * + * @param block Lambda executed if the file doesn't exist or the file is empty + */ + inline fun File.createIfNotExists(block: (File) -> Unit): File { + if (length() == 0L) block(this) + + parentFile.mkdirs() + createNewFile() + return this } /** - * Executes the [block] if the receiver file does not exist. + * Executes the [block] if the receiver file does not exist or is empty. */ inline fun File.ifNotExists(block: (File) -> Unit): File { - if (!exists()) block(this) + if (length() == 0L) block(this) return this } + /** + * Modifies the receiver file if the downloaded file compare check succeeds + * + * @receiver The destination file to write the bytes to + * + * @param url The url to download the file from + * @param compare Compare method. -1 if remote is larger. 0 if both file have the same size. 1 if local is larger + * @param block Configuration block for the request + * + * @return An exception or the file + */ + suspend fun File.downloadCompare( + url: String, + compare: Int, + block: HttpRequestBuilder.() -> Unit = {}, + ) = runCatching { + createIfNotExists { + val bytes = readBytes() + val remote = LambdaHttp.download(url, block) + val sign = (bytes.size - remote.size).sign + + if (sign == compare) writeBytes(remote) + } + } + /** * Downloads the given file url if the file is not present * - * This function does not guarantee that the given file will be created + * @receiver The destination file to write the bytes to + * + * @param url The url to download the file from + * @param block Configuration block for the request + * + * @return An exception or the file */ - fun File.downloadIfNotPresent( + suspend fun File.downloadIfNotPresent( url: String, - success: (ByteArray) -> Unit = {}, - failure: (FuelError) -> Unit = {} - ) = ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + block: HttpRequestBuilder.() -> Unit = {}, + ) = runCatching { createIfNotExists { LambdaHttp.download(url, this, block) } } /** * Downloads the given file url if the file is not present * - * This function does not guarantee that the given file will be created + * @receiver The url to download the file from + * + * @param file The destination file to write the bytes to + * @param block Configuration block for the request + * + * @return An exception or the file */ - fun String.downloadIfNotPresent( + suspend fun String.downloadIfNotPresent( file: File, - success: (ByteArray) -> Unit = {}, - failure: (FuelError) -> Unit = {} - ) = file.ifNotExists { httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } } + block: HttpRequestBuilder.() -> Unit = {}, + ) = runCatching { file.createIfNotExists { LambdaHttp.download(this, file, block) } } /** - * Downloads the given file url if the file is not present + * Lambda that downloads the given file url if the file is not present + * + * @receiver The destination file to write the bytes to + * @param block Configuration block for the request + * + * @return A lambda that returns an exception or the file + */ + fun File.downloadIfNotPresent(block: HttpRequestBuilder.() -> Unit = {}): suspend (String) -> Result = + { url -> runCatching { createIfNotExists { LambdaHttp.download(url, this, block) } } } + + /** + * Downloads the given file url if the file is present * - * This function does not guarantee that the given file will be created + * @receiver The destination file to write the bytes to + * + * @param url The url to download the file from + * @param block Configuration block for the request + * + * @return An exception or the file */ - fun File.downloadIfNotPresent(): (String) -> Unit = - { url -> ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, _ -> } } } + suspend fun File.downloadIfPresent( + url: String, + block: HttpRequestBuilder.() -> Unit = {}, + ) = runCatching { ifExists { LambdaHttp.download(url, this, block) } } /** - * Gets the given url if the file is not present + * Downloads the given file url if the file is present * - * This function does not guarantee that the given file will be created + * @receiver The url to download the file from + * + * @param file The destination file to write the bytes to + * @param block Configuration block for the request + * + * @return An exception or the file */ - fun File.getIfNotPresent(): (String) -> Unit = - { url -> ifNotExists { url.httpGet().responseString().third.getOrNull()?.let { writeText(it) } } } + suspend fun String.downloadIfPresent( + file: File, + block: HttpRequestBuilder.() -> Unit = {}, + ) = runCatching { file.ifExists { LambdaHttp.download(this, file, block) } } } From 1d99fe061a757bde58145cd4a68502ddbea794be Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 17 Apr 2025 15:14:51 -0400 Subject: [PATCH 8/9] Better downloading error handling --- .../src/main/kotlin/com/lambda/network/LambdaHttp.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt b/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt index 224a75624..7ff7fcd0a 100644 --- a/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt +++ b/common/src/main/kotlin/com/lambda/network/LambdaHttp.kt @@ -35,13 +35,17 @@ val LambdaHttp = HttpClient { } suspend inline fun HttpClient.download(url: String, file: File, block: HttpRequestBuilder.() -> Unit = {}) { - val response = get(url, block).readRawBytes() - file.writeBytes(response) + val response = get(url, block) + check(response.status.isSuccess()) { "Download for $url failed with non 2xx status code" } + + file.writeBytes(response.readRawBytes()) } suspend inline fun HttpClient.download(url: String, output: OutputStream, block: HttpRequestBuilder.() -> Unit = {}) { - val response = get(url, block).readRawBytes() - output.write(response) + val response = get(url, block) + check(response.status.isSuccess()) { "Download for $url failed with non 2xx status code" } + + output.write(response.readRawBytes()) } suspend inline fun HttpClient.download(url: String, block: HttpRequestBuilder.() -> Unit) = From b5dd8d1c0ff14b363bce5858ed44540a63488eef Mon Sep 17 00:00:00 2001 From: Constructor Date: Thu, 17 Apr 2025 22:58:25 +0200 Subject: [PATCH 9/9] Added support for arbitrarily deeply nested classes --- .../util/DynamicReflectionSerializer.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index d7b5ffc87..c49381e5e 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -74,9 +74,25 @@ object DynamicReflectionSerializer : Loadable { "${Network.mappings}/${Network.gameVersion}" .downloadIfNotPresent(cache.resolveFile(Network.gameVersion)) .map { file -> - file.readLines() + val standardMappings = file.readLines() .map { it.split(' ') } - .associate { it[0].split('$').last() to it[1] } + .filter { it.size == 2 } + .associate { (obf, deobf) -> obf to deobf } + + buildMap { + putAll(standardMappings) + + standardMappings.forEach { (obf, deobf) -> + put(obf.split('$').last(), deobf) + if ('$' !in obf) return@forEach + put(obf.replace('$', '.'), deobf) + val parts = obf.split('$') + if (!parts.all { it.startsWith("class_") }) return@forEach + (1 until parts.size).forEach { i -> + put("${parts.take(i).joinToString("$")}.${parts.drop(i).joinToString("$")}", deobf) + } + } + } } .getOrElse { LOG.error("Unable to download deobfuscated qualifiers", it) @@ -84,6 +100,7 @@ object DynamicReflectionSerializer : Loadable { } } + val String.remappedName get() = mappings.getOrDefault(this, this) fun Class.dynamicName(remap: Boolean) =