Skip to content

Commit d96bae7

Browse files
committed
Feat: Dynamic remapping
1 parent 0837af3 commit d96bae7

File tree

9 files changed

+200
-68
lines changed

9 files changed

+200
-68
lines changed

common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import com.lambda.brigadier.executeWithResult
2727
import com.lambda.brigadier.required
2828
import com.lambda.command.LambdaCommand
2929
import com.lambda.module.modules.player.Replay
30+
import com.lambda.util.FileUtils.listRecursive
3031
import com.lambda.util.FolderRegister
31-
import com.lambda.util.FolderRegister.listRecursive
3232
import com.lambda.util.extension.CommandBuilder
3333
import kotlin.io.path.exists
3434

common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import com.lambda.event.events.TickEvent
2121
import com.lambda.event.listener.SafeListener.Companion.listen
2222
import com.lambda.module.Module
2323
import com.lambda.module.tag.ModuleTag
24+
import com.lambda.util.FileUtils.locationBoundDirectory
2425
import com.lambda.util.FolderRegister
25-
import com.lambda.util.FolderRegister.locationBoundDirectory
2626
import com.lambda.util.StringUtils.hashString
2727
import com.lambda.util.player.SlotUtils.combined
2828
import com.lambda.util.world.entitySearch

common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import com.lambda.sound.SoundManager.playSound
3838
import com.lambda.util.Communication.info
3939
import com.lambda.util.Communication.logError
4040
import com.lambda.util.Communication.warn
41+
import com.lambda.util.FileUtils.locationBoundDirectory
4142
import com.lambda.util.FolderRegister
42-
import com.lambda.util.FolderRegister.locationBoundDirectory
4343
import com.lambda.util.Formatting.asString
4444
import com.lambda.util.Formatting.getTime
4545
import com.lambda.util.KeyCode

common/src/main/kotlin/com/lambda/network/CapeManager.kt

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.lambda.network
1919

2020
import com.github.kittinunf.fuel.Fuel
2121
import com.github.kittinunf.fuel.core.requests.CancellableRequest
22+
import com.lambda.Lambda
2223
import com.lambda.Lambda.mc
2324
import com.lambda.context.SafeContext
2425
import com.lambda.core.Loadable
@@ -31,11 +32,13 @@ import com.lambda.network.api.v1.models.Cape
3132
import com.lambda.sound.SoundManager.toIdentifier
3233
import com.lambda.util.Communication.info
3334
import com.lambda.util.Communication.logError
35+
import com.lambda.util.FileUtils.downloadIfNotPresent
3436
import com.lambda.util.FolderRegister.capes
3537
import com.lambda.util.extension.get
3638
import com.lambda.util.extension.resolveFile
3739
import net.minecraft.client.texture.NativeImage.read
3840
import net.minecraft.client.texture.NativeImageBackedTexture
41+
import java.io.File
3942
import java.util.UUID
4043
import java.util.concurrent.ConcurrentHashMap
4144
import kotlin.io.path.ExperimentalPathApi
@@ -73,21 +76,18 @@ object CapeManager : ConcurrentHashMap<UUID, String>(), Loadable {
7376
failure = { logError("Could not fetch the cape of the player", it) }
7477
)
7578

76-
private fun SafeContext.download(cape: Cape): CancellableRequest =
77-
Fuel.download(cape.url)
78-
.fileDestination { _, _ -> capes.resolveFile("${cape.id}.png") }
79-
.response { result ->
80-
result.fold(
81-
success = {
82-
val image = TextureUtils.readImage(it)
83-
val native = NativeImageBackedTexture(image)
84-
val id = cape.identifier
79+
private fun SafeContext.download(cape: Cape): File =
80+
capes.resolveFile("${cape.id}.png")
81+
.downloadIfNotPresent(cape.url,
82+
success = {
83+
val image = TextureUtils.readImage(it)
84+
val native = NativeImageBackedTexture(image)
85+
val id = cape.identifier
8586

86-
mc.textureManager.registerTexture(id, native)
87-
},
88-
failure = { logError("Could not download the cape", it) }
89-
)
90-
}
87+
Lambda.mc.textureManager.registerTexture(id, native)
88+
},
89+
failure = { logError("Could not download the cape", it) },
90+
)
9191

9292
override fun load() = "Loaded ${images.size} cached capes"
9393

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.network.api.v1.endpoints
19+
20+
import com.github.kittinunf.fuel.Fuel
21+
import com.github.kittinunf.fuel.core.FuelError
22+
import com.github.kittinunf.fuel.core.requests.CancellableRequest
23+
import com.lambda.module.modules.client.Network.apiUrl
24+
import com.lambda.module.modules.client.Network.apiVersion
25+
import net.minecraft.SharedConstants
26+
27+
/**
28+
* Gets the Minecraft mappings for dynamic remapping
29+
*
30+
* Example:
31+
* - version: 765
32+
*
33+
* response: File or error
34+
*/
35+
fun getMappings(version: String = SharedConstants.VERSION_NAME, success: (String) -> Unit, failure: (FuelError) -> Unit): CancellableRequest =
36+
Fuel.get("$apiUrl/api/${apiVersion.value}/mappings?version=$version")
37+
.responseString { _, _, result -> result.fold(success, failure) }

common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@
1717

1818
package com.lambda.util
1919

20+
import com.lambda.Lambda.LOG
21+
import com.lambda.core.Loadable
22+
import com.lambda.network.api.v1.endpoints.getMappings
23+
import com.lambda.util.FileUtils.getIfNotPresent
24+
import com.lambda.util.FileUtils.ifNotExists
25+
import com.lambda.util.FolderRegister.cache
26+
import com.lambda.util.extension.resolveFile
2027
import com.mojang.serialization.Codec
28+
import net.minecraft.SharedConstants
2129
import net.minecraft.block.BlockState
2230
import net.minecraft.client.resource.language.TranslationStorage
2331
import net.minecraft.item.ItemStack
@@ -34,7 +42,7 @@ import java.lang.reflect.Field
3442
import java.lang.reflect.InaccessibleObjectException
3543
import java.util.*
3644

37-
object DynamicReflectionSerializer {
45+
object DynamicReflectionSerializer : Loadable {
3846
// Classes that should not be recursively serialized
3947
private val skipables = setOf(
4048
Codec::class.java,
@@ -62,6 +70,19 @@ object DynamicReflectionSerializer {
6270

6371
private const val INDENT = 2
6472

73+
val mappings = cache.resolveFile("mappings-${SharedConstants.getProtocolVersion()}.m")
74+
.ifNotExists { getMappings(success = it.getIfNotPresent(), failure = { LOG.error("Could not download the required files for the dynamic remapper") }).join() }
75+
.let { file ->
76+
file.readLines()
77+
.map { it.split('\t') }
78+
.associate { it[0] to it[1] }
79+
}
80+
81+
inline val <T : Any> Class<T>.dynamicName: String get() = mappings.getOrDefault(simpleName, simpleName)
82+
83+
inline val Field.dynamicName: String get() = mappings.getOrDefault(name, name)
84+
85+
6586
// ToDo: To make this work in production, every field could be remapped.
6687
fun Any.dynamicString(
6788
maxRecursionDepth: Int = 6,
@@ -71,12 +92,12 @@ object DynamicReflectionSerializer {
7192
builder: StringBuilder = StringBuilder(),
7293
): String {
7394
if (visitedObjects.contains(this)) {
74-
builder.appendLine("$indent${javaClass.simpleName} (Circular Reference)")
95+
builder.appendLine("$indent${javaClass.dynamicName} (Circular Reference)")
7596
return builder.toString()
7697
}
7798

7899
visitedObjects.add(this)
79-
builder.appendLine("$indent${javaClass.simpleName}")
100+
builder.appendLine("$indent${javaClass.dynamicName}")
80101

81102
val fields = javaClass.declaredFields + javaClass.superclass?.declaredFields.orEmpty()
82103
fields.forEach { field ->
@@ -103,7 +124,7 @@ object DynamicReflectionSerializer {
103124
}
104125
val fieldValue = field.get(this)
105126
val fieldIndent = indent + " ".repeat(INDENT)
106-
builder.appendLine("$fieldIndent${field.name}: ${fieldValue.formatFieldValue()}")
127+
builder.appendLine("$fieldIndent${field.dynamicName}: ${fieldValue.formatFieldValue()}")
107128

108129
if (currentDepth < maxRecursionDepth
109130
&& fieldValue != null
@@ -139,4 +160,8 @@ object DynamicReflectionSerializer {
139160
is RegistryEntry<*> -> "${value()}"
140161
else -> this?.toString() ?: "null"
141162
}
163+
164+
override fun load(): String {
165+
return "Loaded ${mappings.size} remapped named"
166+
}
142167
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.util
19+
20+
import com.github.kittinunf.fuel.core.FuelError
21+
import com.github.kittinunf.fuel.httpDownload
22+
import com.github.kittinunf.fuel.httpGet
23+
import com.github.kittinunf.result.getOrNull
24+
import com.lambda.Lambda.mc
25+
import com.lambda.util.StringUtils.sanitizeForFilename
26+
import java.io.File
27+
import java.net.InetSocketAddress
28+
29+
object FileUtils {
30+
/**
31+
* Returns a sequence of all the files in a tree that matches the [predicate]
32+
*/
33+
fun File.listRecursive(predicate: (File) -> Boolean): Sequence<File> = walk().filter(predicate)
34+
35+
/**
36+
* Ensures the current file exists by creating it if it does not.
37+
*
38+
* If the file already exists, it will not be recreated. The necessary
39+
* parent directories will be created if they do not exist.
40+
*/
41+
fun File.createIfNotExists(): File = also { parentFile.mkdirs(); createNewFile() }
42+
43+
/**
44+
* Retrieves or creates a directory based on the current network connection and world dimension.
45+
*
46+
* The directory is determined by the host name of the current network connection (or "singleplayer" if offline)
47+
* and the dimension key of the current world. These values are sanitized for use as filenames and combined
48+
* to form a path under the current file. If the directory does not exist, it will be created.
49+
*
50+
* @receiver The base directory where the location-bound directory will be created.
51+
* @return A `File` object representing the location-bound directory.
52+
*
53+
* The path is structured as:
54+
* - `[base directory]/[host name]/[dimension key]`
55+
*
56+
* Example:
57+
* If playing on a server with hostname "example.com" and in the "overworld" dimension, the path would be:
58+
* - `[base directory]/example.com/overworld`
59+
*/
60+
fun File.locationBoundDirectory(): File {
61+
val hostName = (mc.networkHandler?.connection?.address as? InetSocketAddress)?.hostName ?: "singleplayer"
62+
val path = resolve(
63+
hostName.sanitizeForFilename()
64+
).resolve(
65+
mc.world?.dimensionKey?.value?.path?.sanitizeForFilename() ?: "unknown" // TODO: Change with utils when merged to master
66+
)
67+
path.mkdirs()
68+
return path
69+
}
70+
71+
/**
72+
* Executes the [block] if the receiver file exists
73+
*/
74+
inline fun File.ifExists(block: (File) -> Unit): File {
75+
if (exists()) block(this)
76+
return this
77+
}
78+
79+
/**
80+
* Executes the [block] if the receiver file does not exist.
81+
*/
82+
inline fun File.ifNotExists(block: (File) -> Unit): File {
83+
if (!exists()) block(this)
84+
return this
85+
}
86+
87+
/**
88+
* Downloads the given file url if the file is not present
89+
*
90+
* This function does not guarantee that the given file will be created
91+
*/
92+
fun File.downloadIfNotPresent(url: String, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File =
93+
ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } }
94+
95+
/**
96+
* Downloads the given file url if the file is not present
97+
*
98+
* This function does not guarantee that the given file will be created
99+
*/
100+
fun String.downloadIfNotPresent(file: File, success: (ByteArray) -> Unit = {}, failure: (FuelError) -> Unit = {}): File =
101+
file.ifNotExists { httpDownload().fileDestination { _, _ -> it }.response { _, _, result -> result.fold(success, failure) } }
102+
103+
/**
104+
* Downloads the given file url if the file is not present
105+
*
106+
* This function does not guarantee that the given file will be created
107+
*/
108+
fun File.downloadIfNotPresent(): (String) -> Unit {
109+
return { url -> ifNotExists { url.httpDownload().fileDestination { _, _ -> it }.response { _, _, _ -> } } } }
110+
111+
/**
112+
* Gets the given url if the file is not present
113+
*
114+
* This function does not guarantee that the given file will be created
115+
*/
116+
fun File.getIfNotPresent(): (String) -> Unit { return { url -> ifNotExists { url.httpGet().responseString().third.getOrNull()?.let { writeText(it) } } } }
117+
}

common/src/main/kotlin/com/lambda/util/FolderRegister.kt

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ import com.lambda.util.FolderRegister.lambda
2424
import com.lambda.util.FolderRegister.minecraft
2525
import com.lambda.util.FolderRegister.packetLogs
2626
import com.lambda.util.FolderRegister.replay
27-
import com.lambda.util.StringUtils.sanitizeForFilename
28-
import java.io.File
29-
import java.net.InetSocketAddress
3027
import java.nio.file.Path
3128
import kotlin.io.path.createDirectories
3229
import kotlin.io.path.notExists
@@ -62,45 +59,4 @@ object FolderRegister : Loadable {
6259
"Created directories: ${createdFolders.joinToString { minecraft.parent.relativize(it).toString() }}"
6360
} else "Loaded ${folders.size} directories"
6461
}
65-
66-
/**
67-
* Ensures the current file exists by creating it if it does not.
68-
*
69-
* If the file already exists, it will not be recreated. The necessary
70-
* parent directories will be created if they do not exist.
71-
*/
72-
fun File.createIfNotExists(): File = also { parentFile.mkdirs(); createNewFile() }
73-
74-
/**
75-
* Returns a sequence of all the files in a tree that matches the [predicate]
76-
*/
77-
fun File.listRecursive(predicate: (File) -> Boolean) = walk().filter(predicate)
78-
79-
/**
80-
* Retrieves or creates a directory based on the current network connection and world dimension.
81-
*
82-
* The directory is determined by the host name of the current network connection (or "singleplayer" if offline)
83-
* and the dimension key of the current world. These values are sanitized for use as filenames and combined
84-
* to form a path under the current file. If the directory does not exist, it will be created.
85-
*
86-
* @receiver The base directory where the location-bound directory will be created.
87-
* @return A `File` object representing the location-bound directory.
88-
*
89-
* The path is structured as:
90-
* - `[base directory]/[host name]/[dimension key]`
91-
*
92-
* Example:
93-
* If playing on a server with hostname "example.com" and in the "overworld" dimension, the path would be:
94-
* - `[base directory]/example.com/overworld`
95-
*/
96-
fun File.locationBoundDirectory(): File {
97-
val hostName = (mc.networkHandler?.connection?.address as? InetSocketAddress)?.hostName ?: "singleplayer"
98-
val path = resolve(
99-
hostName.sanitizeForFilename()
100-
).resolve(
101-
mc.world?.dimensionKey?.value?.path?.sanitizeForFilename() ?: "unknown" // TODO: Change with utils when merged to master
102-
)
103-
path.mkdirs()
104-
return path
105-
}
10662
}

common/src/main/kotlin/com/lambda/util/extension/Other.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ import net.minecraft.client.texture.TextureManager
2323
import net.minecraft.util.Identifier
2424
import java.io.File
2525
import java.nio.file.Path
26-
import kotlin.contracts.ExperimentalContracts
27-
import kotlin.contracts.InvocationKind
28-
import kotlin.contracts.contract
2926

3027
val GameProfile.isOffline
3128
get() = properties.isEmpty

0 commit comments

Comments
 (0)