From 3a3feba68be13e0a0e65e5d86073a77f5d11ea99 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:57:09 -0500 Subject: [PATCH 01/30] aaaaaaa --- .../kotlin/com/lambda/util/extension/Nbt.kt | 14 ++++ .../com/lambda/util/extension/Structures.kt | 71 ++++++++++++++++++- .../kotlin/com/lambda/util/math/MathUtils.kt | 1 + .../src/main/resources/lambda.accesswidener | 5 ++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt b/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt index 95aff822e..5ff40be37 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt @@ -20,6 +20,7 @@ package com.lambda.util.extension import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtInt import net.minecraft.nbt.NbtList +import net.minecraft.util.math.Vec3i /** * Puts a list of integer into the component, this is not the same as an int array @@ -27,3 +28,16 @@ import net.minecraft.nbt.NbtList fun NbtCompound.putIntList(key: String, vararg values: Int) { put(key, values.fold(NbtList()) { list, value -> list.add(NbtInt.of(value)); list }) } + +/** + * Retrieves a vector from a tuple + */ +fun NbtCompound.getVector(key: String): Vec3i { + // TODO: Handle other cases like array of values, capitalized keys, etc + val compound = getCompound(key) + val x = compound.getInt("x") + val y = compound.getInt("y") + val z = compound.getInt("z") + + return Vec3i(x, y, z) +} diff --git a/common/src/main/kotlin/com/lambda/util/extension/Structures.kt b/common/src/main/kotlin/com/lambda/util/extension/Structures.kt index 04358f4aa..775fdf8bf 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Structures.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Structures.kt @@ -19,6 +19,7 @@ package com.lambda.util.extension import com.lambda.Lambda.mc import com.lambda.util.VarIntIterator +import com.lambda.util.math.MathUtils.logCap import com.lambda.util.world.FastVector import com.lambda.util.world.fastVectorOf import com.lambda.util.world.x @@ -31,6 +32,7 @@ import net.minecraft.nbt.NbtList import net.minecraft.registry.RegistryEntryLookup import net.minecraft.structure.StructureTemplate import kotlin.experimental.and +import kotlin.math.abs private fun positionFromIndex(width: Int, length: Int, index: Int): FastVector { val y = index / (width * length) @@ -155,4 +157,71 @@ private fun StructureTemplate.readSpongeV3OrException( fun StructureTemplate.readLitematicaOrException( lookup: RegistryEntryLookup, nbt: NbtCompound, -): Throwable = NotImplementedError("Litematica is not supported, you can help by contributing to the project") +): Throwable? { + val version = nbt.getInt("MinecraftDataVersion") + + val metadata = nbt.getCompound("Metadata") + val author = metadata.getString("Author") + + val dimension = metadata.getVector("EnclosingSize") + + val newPalette = NbtList() + val newBlocks = NbtList() + + val regions = nbt.getCompound("Regions") + regions.keys.map { regions.getCompound(it) } + .forEach { + val position = it.getVector("Position") + val size = it.getVector("Size") + + val xSizeAbs = abs(size.x) + val ySizeAbs = abs(size.y) + val zSizeAbs = abs(size.z) + + if (size.x < 0) position.x %= size.x + 1 + if (size.y < 0) position.y %= size.y + 1 + if (size.z < 0) position.z %= size.z + 1 + + // The litematic's block state palette is the same as nbt + newPalette.addAll(it.getList("BlockStatePalette", 10)) + + val palette = it.getLongArray("BlockStates") + val bits = palette.size.logCap(2) + val maxEntryValue = (1 shl bits) - 1L + + for (y in 0 until ySizeAbs) { + for (z in 0 until zSizeAbs) { + for (x in 0 until xSizeAbs) { + val index = (y * xSizeAbs * zSizeAbs) + z * xSizeAbs + x + + val startOffset = index * bits + val startArrIndex = startOffset / 64 + val endArrIndex = ((index + 1) * bits - 1) / 64 + val startBitOffset = startOffset % 64 + + val stateId = + if (startArrIndex == endArrIndex) palette[startArrIndex] ushr startBitOffset and maxEntryValue + else (palette[startArrIndex] ushr startBitOffset or palette[endArrIndex] shl (64 - startBitOffset)) and maxEntryValue + + newBlocks.add(NbtCompound().apply { + putIntList("pos", x, y, z) + putInt("state", stateId.toInt()) + }) + } + } + } + } + + // Construct a structure compatible nbt compound + nbt.putInt("DataVersion", version) + nbt.putIntList("size", dimension.x, dimension.y, dimension.z) + nbt.put("palette", newPalette) + nbt.put("blocks", newBlocks) + nbt.putString("author", author) + + // Fix the data for future versions + DataFixTypes.STRUCTURE.update(mc.dataFixer, nbt, version) + + // Use the StructureTemplate NBT read utils in order to construct the template + return readNbtOrException(lookup, nbt) +} diff --git a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt index f6a889191..8d9ac1355 100644 --- a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt @@ -38,6 +38,7 @@ object MathUtils { fun Double.floorToInt() = floor(this).toInt() fun Double.ceilToInt() = ceil(this).toInt() + fun Int.logCap(minimum: Int) = max(minimum.toDouble(), ceil(log2(toDouble()))).toInt() fun T.roundToStep(step: T): T { val stepD = step.toDouble() diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index e916c247e..1526daae9 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -64,6 +64,11 @@ accessible field net/minecraft/network/ClientConnection packetsReceivedCounter I accessible field net/minecraft/network/packet/c2s/login/LoginKeyC2SPacket encryptedSecretKey [B accessible field net/minecraft/network/packet/c2s/login/LoginKeyC2SPacket nonce [B +# Math +accessible method net/minecraft/util/math/Vec3i setX (I)Lnet/minecraft/util/math/Vec3i; +accessible method net/minecraft/util/math/Vec3i setY (I)Lnet/minecraft/util/math/Vec3i; +accessible method net/minecraft/util/math/Vec3i setZ (I)Lnet/minecraft/util/math/Vec3i; + # Other accessible field net/minecraft/world/explosion/Explosion behavior Lnet/minecraft/world/explosion/ExplosionBehavior; accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List; From a86326739ef4505ca4e38917ed815d373fb78a97 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:58:20 -0500 Subject: [PATCH 02/30] Discord and Network refactor --- build.gradle.kts | 8 +- common/src/main/kotlin/com/lambda/Lambda.kt | 2 + .../{RpcCommand.kt => DiscordCommand.kt} | 12 +- .../com/lambda/event/listener/SafeListener.kt | 20 +-- .../lambda/event/listener/UnsafeListener.kt | 26 ++-- .../lambda/http/api/rpc/v1/endpoints/Login.kt | 41 ----- .../client/{DiscordRPC.kt => Discord.kt} | 144 ++++++------------ .../lambda/module/modules/client/Network.kt | 126 +++++++++++++++ .../api}/v1/endpoints/CreateParty.kt | 32 ++-- .../api}/v1/endpoints/DeleteParty.kt | 16 +- .../api}/v1/endpoints/GetParty.kt | 18 +-- .../api}/v1/endpoints/JoinParty.kt | 22 +-- .../api}/v1/endpoints/LeaveParty.kt | 18 +-- .../lambda/network/api/v1/endpoints/Login.kt | 38 +++++ .../api}/v1/endpoints/UpdateParty.kt | 30 ++-- .../api}/v1/models/Authentication.kt | 2 +- .../rpc => network/api}/v1/models/Party.kt | 14 +- .../rpc => network/api}/v1/models/Player.kt | 2 +- .../rpc => network/api}/v1/models/Settings.kt | 2 +- forge/build.gradle.kts | 2 +- 20 files changed, 330 insertions(+), 245 deletions(-) rename common/src/main/kotlin/com/lambda/command/commands/{RpcCommand.kt => DiscordCommand.kt} (83%) delete mode 100644 common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/Login.kt rename common/src/main/kotlin/com/lambda/module/modules/client/{DiscordRPC.kt => Discord.kt} (59%) create mode 100644 common/src/main/kotlin/com/lambda/module/modules/client/Network.kt rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/CreateParty.kt (57%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/DeleteParty.kt (67%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/GetParty.kt (70%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/JoinParty.kt (65%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/LeaveParty.kt (70%) create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/endpoints/UpdateParty.kt (58%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/models/Authentication.kt (96%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/models/Party.kt (84%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/models/Player.kt (96%) rename common/src/main/kotlin/com/lambda/{http/api/rpc => network/api}/v1/models/Settings.kt (96%) diff --git a/build.gradle.kts b/build.gradle.kts index f427633a0..99833a2b9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -85,6 +85,12 @@ subprojects { if (path == ":common") return@subprojects + loom.runs { + all { + property("lambda.dev", "youtu.be/RYnFIRc0k6E") + } + } + tasks { register("renderDoc") { val javaHome = Jvm.current().javaHome @@ -144,7 +150,7 @@ allprojects { tasks { compileKotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget = JvmTarget.JVM_17 } } } diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index dc20b593c..a88c0b570 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -52,6 +52,8 @@ object Lambda { @JvmStatic val mc: MinecraftClient by lazy { MinecraftClient.getInstance() } + val isDebug = System.getProperty("lambda.dev") != null + val gson: Gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(ModuleTag::class.java, ModuleTagSerializer) diff --git a/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt similarity index 83% rename from common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt rename to common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt index 1de9513db..04beaf0f3 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt @@ -23,19 +23,19 @@ import com.lambda.brigadier.argument.word import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand -import com.lambda.module.modules.client.DiscordRPC +import com.lambda.module.modules.client.Discord import com.lambda.util.extension.CommandBuilder -object RpcCommand : LambdaCommand( - name = "rpc", - description = "Discord Rich Presence commands.", - usage = "rpc " +object DiscordCommand : LambdaCommand( + name = "discord", + description = "Discord Rich Presence commands", + usage = "rpc " ) { override fun CommandBuilder.create() { required(literal("join")) { required(word("id")) { id -> execute { - DiscordRPC.join(id().value()) + Discord.join(id().value()) } } } diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index f1cf5942e..b2b3620fb 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -132,8 +132,8 @@ class SafeListener( /** * This function registers a new [SafeListener] for a generic [Event] type [T]. - * The [transform] is executed on the same thread where the [Event] was dispatched. - * The [transform] will only be executed when the context satisfies certain safety conditions. + * The [predicate] is executed on the same thread where the [Event] was dispatched. + * The [predicate] will only be executed when the context satisfies certain safety conditions. * These conditions are met when none of the following [SafeContext] properties are null: * - [SafeContext.world] * - [SafeContext.player] @@ -142,7 +142,7 @@ class SafeListener( * * This typically occurs when the user is in-game. * - * After the [transform] is executed once, the [SafeListener] will be automatically unsubscribed. + * After the [predicate] is executed once, the [SafeListener] will be automatically unsubscribed. * * Usage: * ```kotlin @@ -156,24 +156,20 @@ class SafeListener( * @param T The type of the event to listen for. This should be a subclass of Event. * @param priority The priority of the listener. Listeners with higher priority will be executed first. The Default value is 0. * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. - * @param transform The function used to transform the event into a value. * @return The newly created and registered [SafeListener]. */ - inline fun Any.listenOnce( + inline fun Any.listenOnce( priority: Int = 0, alwaysListen: Boolean = false, noinline predicate: SafeContext.(T) -> Boolean = { true }, - noinline transform: SafeContext.(T) -> E? = { null }, - ): ReadWriteProperty { - val pointer = Pointer() + ): ReadWriteProperty { + val pointer = Pointer() val destroyable by selfReference> { SafeListener(priority, this@listenOnce, alwaysListen) { event -> - pointer.value = transform(event) + pointer.value = event - if (predicate(event) && - pointer.value != null - ) { + if (predicate(event)) { val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) } diff --git a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt index 221617062..a4abc74fb 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/UnsafeListener.kt @@ -21,8 +21,11 @@ import com.lambda.context.SafeContext import com.lambda.event.Event import com.lambda.event.EventFlow import com.lambda.event.Muteable +import com.lambda.threading.runConcurrent import com.lambda.util.Pointer import com.lambda.util.selfReference +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -136,24 +139,20 @@ class UnsafeListener( * @param T The type of the event to listen for. This should be a subclass of Event. * @param priority The priority of the listener. Listeners with higher priority will be executed first. * @param alwaysListen If true, the listener will be executed even if it is muted. - * @param transform The function used to transform the event into a value. * @return The newly created and registered [UnsafeListener]. */ - inline fun Any.listenOnceUnsafe( + inline fun Any.listenOnceUnsafe( priority: Int = 0, alwaysListen: Boolean = false, - noinline transform: (T) -> E? = { null }, - noinline predicate: (T) -> Boolean = { true }, - ): ReadWriteProperty { - val pointer = Pointer() + noinline function: (T) -> Boolean = { true }, + ): ReadWriteProperty { + val pointer = Pointer() val destroyable by selfReference> { UnsafeListener(priority, this@listenOnceUnsafe, alwaysListen) { event -> - pointer.value = transform(event) + pointer.value = event - if (predicate(event) && - pointer.value != null - ) { + if (function(event)) { val self by this@selfReference EventFlow.syncListeners.unsubscribe(self) } @@ -194,10 +193,13 @@ class UnsafeListener( inline fun Any.listenUnsafeConcurrently( priority: Int = 0, alwaysListen: Boolean = false, - noinline function: (T) -> Unit = {}, + scheduler: CoroutineDispatcher = Dispatchers.Default, + noinline function: suspend (T) -> Unit = {}, ): UnsafeListener { val listener = UnsafeListener(priority, this, alwaysListen) { event -> - function(event) + runConcurrent(scheduler) { + function(event) + } } EventFlow.concurrentListeners.subscribe(listener) diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/Login.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/Login.kt deleted file mode 100644 index 42335c5d0..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/Login.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2024 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.http.api.rpc.v1.endpoints - -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Authentication - -fun login( - endpoint: String, - version: String, - - // The player's Discord token. - // example: OTk1MTU1NzcyMzYxMTQ2NDM4 - discordToken: String, - - // The player's username. - // example: "Notch" - username: String, - - // The player's Mojang session hash. - // example: 069a79f444e94726a5befca90e38aaf5 - hash: String, -) = - Fuel.post("$endpoint/api/$version/login", listOf("token" to discordToken, "username" to username, "hash" to hash)) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt rename to common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 24d216775..69562724e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -19,18 +19,24 @@ package com.lambda.module.modules.client import com.lambda.Lambda import com.lambda.Lambda.LOG -import com.lambda.Lambda.mc -import com.lambda.event.EventFlow +import com.lambda.context.SafeContext import com.lambda.event.events.ConnectionEvent -import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe -import com.lambda.http.api.rpc.v1.endpoints.* -import com.lambda.http.api.rpc.v1.models.Authentication -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently +import com.lambda.network.api.v1.models.Party import com.lambda.module.Module +import com.lambda.module.modules.client.Network.discordAuth +import com.lambda.module.modules.client.Network.isAuthenticated +import com.lambda.module.modules.client.Network.rpc +import com.lambda.module.modules.client.Network.apiAuth import com.lambda.module.tag.ModuleTag +import com.lambda.network.api.v1.endpoints.createParty +import com.lambda.network.api.v1.endpoints.editParty +import com.lambda.network.api.v1.endpoints.joinParty +import com.lambda.network.api.v1.endpoints.leaveParty import com.lambda.threading.runConcurrent -import com.lambda.util.Communication.logError -import com.lambda.util.Communication.warn +import com.lambda.threading.runSafe +import com.lambda.util.Communication +import com.lambda.util.Communication.toast import com.lambda.util.Nameable import com.lambda.util.StringUtils.capitalize import dev.cbyrne.kdiscordipc.KDiscordIPC @@ -39,15 +45,12 @@ import dev.cbyrne.kdiscordipc.core.event.impl.ActivityJoinEvent import dev.cbyrne.kdiscordipc.core.event.impl.ActivityJoinRequestEvent import dev.cbyrne.kdiscordipc.core.event.impl.ErrorEvent import dev.cbyrne.kdiscordipc.core.event.impl.ReadyEvent -import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket import dev.cbyrne.kdiscordipc.data.activity.* import kotlinx.coroutines.delay import net.minecraft.entity.player.PlayerEntity -import net.minecraft.network.encryption.NetworkEncryptionUtils -import java.math.BigInteger -object DiscordRPC : Module( - name = "DiscordRPC", +object Discord : Module( + name = "Discord", description = "Discord Rich Presence configuration", defaultTags = setOf(ModuleTag.CLIENT), // enabledByDefault = true, // ToDo: Bring this back on beta release @@ -55,6 +58,7 @@ object DiscordRPC : Module( private val page by setting("Page", Page.General) /* General settings */ + private val delay by setting("Update Delay", 15000L, 15000L..30000L, 100L, unit = "ms") { page == Page.General } private val showTime by setting("Show Time", true, description = "Show how long you have been playing for.") { page == Page.General } private val line1Left by setting("Line 1 Left", LineInfo.WORLD) { page == Page.General } private val line1Right by setting("Line 1 Right", LineInfo.USERNAME) { page == Page.General } @@ -63,56 +67,34 @@ object DiscordRPC : Module( private val confirmCoordinates by setting("Show Coordinates", false, description = "Confirm display the player coordinates") { page == Page.General } private val confirmServer by setting("Show Server IP", false, description = "Confirm display the server IP") { page == Page.General } - /* Technical settings */ - private var rpcServer by setting("RPC Server", "https://api.lambda-client.org") { page == Page.Settings } - private var apiVersion by setting("API Version", ApiVersion.V1) { page == Page.Settings } - private val delay by setting("Update Delay", 15000L, 15000L..30000L, 100L, unit = "ms") { page == Page.Settings } - /* Party settings */ - private val enableParty by setting("Enable Party", true, description = "Allows you to create parties.") { page == Page.Party } - private val maxPlayers by setting("Max Players", 10, 2..20) { page == Page.Party }.onValueChange { _, _ -> if (player.isPartyOwner) edit() } + private val enableParty by setting("Enable Party", true, description = "Allows you to create parties.") { page == Page.Party } // ToDo: Change this for create by default instead + private val maxPlayers by setting("Max Players", 10, 2..20) { page == Page.Party }.onValueChange { _, _ -> if (player.isPartyOwner) edit() } // ToDo: Avoid spam requests - private val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) private var startup = System.currentTimeMillis() - private val dimensionRegex = Regex("""\b\w+_\w+\b""") + private val dimensionRegex = Regex("""\b\w+_\w+\b""") // ToDo: Change this when combat is merged private var ready: ReadyEvent? = null - private var keyEvent: ConnectionEvent.Connect.Login.EncryptionResponse? = null - - private var discordAuth: AuthenticatePacket.Data? = null - private var rpcAuth: Authentication? = null private var currentParty: Party? = null - private var connectionTime: Long = 0 - private var serverId: String? = null private val isPartyInteractionAllowed: Boolean - get() = rpcAuth != null && discordAuth != null + get() = apiAuth != null && discordAuth != null - private val PlayerEntity.isPartyOwner + val PlayerEntity.isPartyOwner get() = uuid == currentParty?.leader?.uuid - private val PlayerEntity.isInParty + val PlayerEntity.isInParty get() = currentParty?.players?.any { it.uuid == this.uuid } init { - listenUnsafe { - connectionTime = System.currentTimeMillis() - serverId = it.serverId - } - - listenUnsafe { - if (it.secretKey.isDestroyed) - return@listenUnsafe logError( - "Error during the login process", - "The client secret key was destroyed by another listener" - ) - - keyEvent = it + listenUnsafeConcurrently { + // FixMe: We have to wait even though this is the last event until toSafe() != null + // because of timing + delay(3000) + runSafe { connect() } } - listenUnsafe { connect() } - - // TODO: Exponential backoff up to 25 seconds + // TODO: Exponential backoff up to 25 seconds to avoid being rate limited by discord onEnable { connect() } onDisable { disconnect() } } @@ -120,8 +102,8 @@ object DiscordRPC : Module( fun createParty() { if (!isPartyInteractionAllowed) return - val (party, error) = createParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return, maxPlayers, true) - if (error != null) warn(error.toString()) // TODO: Replace with network manager + val (party, error) = createParty(maxPlayers, true) + if (error != null) toast("Failed to create a party: ${error.message}", Communication.LogLevel.WARN) currentParty = party } @@ -130,8 +112,8 @@ object DiscordRPC : Module( fun join(id: String) { if (!isPartyInteractionAllowed) return - val (party, error) = joinParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return, id) - if (error != null) warn("Failed to join the party", error.toString()) + val (party, error) = joinParty(id) + if (error != null) toast("Failed to join the party: ${error.message}", Communication.LogLevel.WARN) currentParty = party } @@ -140,22 +122,19 @@ object DiscordRPC : Module( private fun edit() { if (!isPartyInteractionAllowed) return - val (party, error) = editParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return, maxPlayers) - if (error != null) warn("Failed to edit the party", error.toString()) + val (party, error) = editParty(maxPlayers) + if (error != null) toast("Failed to edit the party: ${error.message}", Communication.LogLevel.WARN) currentParty = party } - private fun connect() { - runConcurrent { rpc.connect() } - - runConcurrent { - keyEvent?.let { rpc.register(it) } - } - + private fun SafeContext.connect() { + // FixMe: Race condition + runConcurrent { rpc.connect() } // ToDo: Duplicate rpc connection network and discord + runConcurrent { rpc.register() } runConcurrent { while (rpc.connected) { - updateActivity() + update() delay(delay) } } @@ -164,28 +143,25 @@ object DiscordRPC : Module( private fun disconnect() { if (rpc.connected) { LOG.info("Gracefully disconnecting from Discord RPC.") - leaveParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return) + leaveParty() rpc.disconnect() } ready = null - discordAuth = null - rpcAuth = null currentParty = null - keyEvent = null } - private suspend fun updateActivity() { + private suspend fun SafeContext.update() { val party = currentParty rpc.activityManager.setActivity { - details = "${line1Left.value()} | ${line1Right.value()}".take(128) - state = "${line2Left.value()} | ${line2Right.value()}".take(128) + details = "${line1Left.value(this@update)} | ${line1Right.value(this@update)}".take(128) + state = "${line2Left.value(this@update)} | ${line2Right.value(this@update)}".take(128) largeImage("lambda", Lambda.VERSION) smallImage("https://mc-heads.net/avatar/${mc.gameProfile.id}/nohelm", mc.gameProfile.name) - if (isPartyInteractionAllowed && party != null) { + if (isAuthenticated && party != null) { party(party.id.toString(), party.players.size, party.settings.maxPlayers) secrets(party.joinSecret) } else { @@ -196,7 +172,7 @@ object DiscordRPC : Module( } } - private suspend fun KDiscordIPC.register(auth: ConnectionEvent.Connect.Login.EncryptionResponse) { + private suspend fun KDiscordIPC.register() { on { ready = this @@ -204,29 +180,12 @@ object DiscordRPC : Module( subscribe(DiscordEvent.ActivityJoinRequest) subscribe(DiscordEvent.ActivityJoin) - if (System.currentTimeMillis() - connectionTime > 300000) { - warn("The authentication hash has expired, reconnect to the server.") - return@on - } - - val hash = BigInteger( - NetworkEncryptionUtils.computeServerId(serverId ?: return@on, auth.publicKey, auth.secretKey) - ).toString(16) - - // Prompt the user to authorize - discordAuth = rpc.applicationManager.authenticate() - - val (authResponse, error) = login(rpcServer, apiVersion.value, discordAuth?.accessToken ?: "", mc.session.username, hash) - if (error != null) warn("Failed to authenticate with the RPC server: ${error.message}") - - rpcAuth = authResponse - if (enableParty) createParty() } // Event when someone would like to join your party on { - LOG.info("The user ${data.userId} has invited you") + toast("The user ${data.userId} has invited you") rpc.activityManager.acceptJoinRequest(data.userId) } @@ -241,10 +200,10 @@ object DiscordRPC : Module( } private enum class Page { - General, Settings, Party + General, Party } - private enum class LineInfo(val value: () -> String) : Nameable { + private enum class LineInfo(val value: SafeContext.() -> String) : Nameable { VERSION({ Lambda.VERSION }), WORLD({ when { @@ -271,9 +230,4 @@ object DiscordRPC : Module( }), FPS({ "${mc.currentFps} FPS" }); } - - private enum class ApiVersion(val value: String) { - // We can use @Deprecated("Not supported") to remove old API versions in the future - V1("v1"), - } } 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 new file mode 100644 index 000000000..b4ba7f5f0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt @@ -0,0 +1,126 @@ +/* + * 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.module.modules.client + +import com.github.kittinunf.fuel.core.FuelManager +import com.lambda.Lambda +import com.lambda.Lambda.mc +import com.lambda.event.EventFlow +import com.lambda.event.events.ClientEvent +import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionRequest +import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionResponse +import com.lambda.event.listener.UnsafeListener.Companion.listenOnceUnsafe +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently +import com.lambda.network.api.v1.endpoints.login +import com.lambda.network.api.v1.models.Authentication +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runConcurrent +import com.lambda.util.Communication +import com.lambda.util.Communication.toast +import com.lambda.util.Communication.warn +import dev.cbyrne.kdiscordipc.KDiscordIPC +import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket +import kotlinx.coroutines.delay +import net.minecraft.client.network.AllowedAddressResolver +import net.minecraft.client.network.ClientLoginNetworkHandler +import net.minecraft.client.network.ServerAddress +import net.minecraft.network.ClientConnection +import net.minecraft.network.NetworkSide.CLIENTBOUND +import net.minecraft.network.encryption.NetworkEncryptionUtils +import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket +import net.minecraft.text.Text +import java.math.BigInteger + +object Network : Module( + name = "Network", + description = "...", + defaultTags = setOf(ModuleTag.CLIENT), + enabledByDefault = true, +) { + var authServer by setting("Auth Server", "auth.lambda-client.org") + var apiUrl: String by setting("API Server", "https://api.lambda-client.org").onValueChange { _, to -> FuelManager.instance.basePath = "$to/api/$apiVersion" } + var apiVersion by setting("API Version", ApiVersion.V1).onValueChange { _, to -> FuelManager.instance.basePath = "$apiUrl/api/$to" } + + var discordAuth: AuthenticatePacket.Data? = null; private set + var apiAuth: Authentication? = null; private set // TODO: Cache + val accessToken: String + get() = apiAuth?.accessToken ?: "" + + val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) + + val isAuthenticated: Boolean + get() = discordAuth != null && apiAuth != null + + private lateinit var serverId: String + private lateinit var hash: String + + init { + FuelManager.instance.basePath = "${apiUrl}/api/${apiVersion}" + + listenUnsafe { + serverId = it.serverId + } + + listenOnceUnsafe { event -> + if (event.secretKey.isDestroyed) return@listenOnceUnsafe false + + hash = BigInteger( + NetworkEncryptionUtils.computeServerId(serverId, event.publicKey, event.secretKey) + ).toString(16) + + val (authResponse, error) = login(discordAuth?.accessToken ?: "", mc.session.username, hash) + if (error != null) { + toast("Unable to authenticate with the API", Communication.LogLevel.DEBUG) + return@listenOnceUnsafe false + } + + apiAuth = authResponse + + // Destroy the listener + true + } + + listenUnsafeConcurrently { + // TODO: add exponential backoff retries + runConcurrent { rpc.connect() } // TODO: Create a function that will wait until x seconds has passed or if the connection is successful + delay(1000) // hack + + discordAuth = rpc.applicationManager.authenticate() + + val addddd = ServerAddress.parse(authServer) + val connection = ClientConnection(CLIENTBOUND) + val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) + .map { it.inetSocketAddress }.get() + + ClientConnection.connect(addr, mc.options.shouldUseNativeTransport(), connection) + .syncUninterruptibly() + + val handler = ClientLoginNetworkHandler(connection, mc, null, null, false, null) { Text.empty() } + + connection.connect(addr.hostName, addr.port, handler) + connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) + } + } + + enum class ApiVersion(val value: String) { + // We can use @Deprecated("Not supported") to remove old API versions in the future + V1("v1"), + } +} diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt similarity index 57% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt index fae958bf2..8f4c98597 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt @@ -15,27 +15,25 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party fun createParty( - endpoint: String, - version: String, - accessToken: String, + // The maximum number of players in the party. + // example: 10 + maxPlayers: Int = 10, - // The maximum number of players in the party. - // example: 10 - maxPlayers: Int = 10, - - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - public: Boolean = true, + // Whether the party is public or not. + // If false can only be joined by invite. + // example: true + public: Boolean = true, ) = - Fuel.post("$endpoint/api/$version/party/create", listOf( - "max_players" to maxPlayers, - "public" to public, - )).responseObject().third + Fuel.post("/party/create", listOf("max_players" to maxPlayers, "public" to public)) + .authentication() + .bearer(Network.accessToken) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt similarity index 67% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt index 786c59084..b01e4dd07 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt @@ -15,15 +15,19 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party fun deleteParty( - endpoint: String, - version: String, + endpoint: String, + version: String, ) = - Fuel.delete("$endpoint/api/$version/party/delete") - .responseObject().third + Fuel.delete("$endpoint/api/$version/party/delete") + .authentication() + .bearer(Network.accessToken) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt similarity index 70% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt index f957b7095..a2badb7c6 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt @@ -15,16 +15,16 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party -fun createParty( - endpoint: String, - version: String, - accessToken: String, -) = - Fuel.get("$endpoint/api/$version/party") - .responseObject().third +fun getParty() = + Fuel.get("/party") + .authentication() + .bearer(Network.accessToken) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt similarity index 65% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt index 25dc8c291..01582dd82 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt @@ -15,20 +15,20 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party fun joinParty( - endpoint: String, - version: String, - accessToken: String, - - // The ID of the party. - // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - partyId: String, + // The ID of the party. + // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + partyId: String, ) = - Fuel.put("$endpoint/api/$version/party/join", listOf("id" to partyId)) - .responseObject().third + Fuel.put("/party/join", listOf("id" to partyId)) + .authentication() + .bearer(Network.accessToken) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt similarity index 70% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt index 8763924db..950723e28 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt @@ -15,16 +15,16 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party -fun leaveParty( - endpoint: String, - version: String, - accessToken: String, -) = - Fuel.put("$endpoint/api/$version/party/leave") - .responseObject().third +fun leaveParty() = + Fuel.put("/party/leave") + .authentication() + .bearer(Network.accessToken) + .responseObject().third 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 new file mode 100644 index 000000000..7125b0c6e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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.gson.responseObject +import com.lambda.network.api.v1.models.Authentication + +fun login( + // The player's Discord token. + // example: OTk1MTU1NzcyMzYxMTQ2NDM4 + discordToken: String, + + // The player's username. + // example: "Notch" + username: String, + + // The player's Mojang session hash. + // example: 069a79f444e94726a5befca90e38aaf5 + hash: String, +) = + Fuel.post("/login", listOf("token" to discordToken, "username" to username, "hash" to hash)) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt similarity index 58% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt index 25f38f82c..dec5f9b1b 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt @@ -15,25 +15,25 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.modules.client.Network +import com.lambda.network.api.v1.models.Party fun editParty( - endpoint: String, - version: String, - accessToken: String, + // The maximum number of players in the party. + // example: 10 + maxPlayers: Int = 10, - // The maximum number of players in the party. - // example: 10 - maxPlayers: Int = 10, - - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - // public: Boolean = true, + // Whether the party is public or not. + // If false can only be joined by invite. + // example: true + // public: Boolean = true, ) = - Fuel.patch("$endpoint/api/$version/party/edit", listOf("max_players" to maxPlayers)) - .responseObject().third + Fuel.patch("/party/edit", listOf("max_players" to maxPlayers)) + .authentication() + .bearer(Network.accessToken) + .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt similarity index 96% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt index 3a4f4774f..d8886535d 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.models +package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt similarity index 84% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt index f2011d039..d0bef2f29 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.models +package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName import java.util.* @@ -23,28 +23,28 @@ import java.util.* data class Party( // The ID of the party. // It is a random string of 30 characters. - @SerializedName("id") + @SerializedName("id") val id: UUID, // The join secret of the party. // It is a random string of 100 characters. - @SerializedName("join_secret") + @SerializedName("join_secret") val joinSecret: String, // The leader of the party - @SerializedName("leader") + @SerializedName("leader") val leader: Player, // The creation date of the party. // example: 2021-10-10T12:00:00Z - @SerializedName("creation") + @SerializedName("creation") val creation: String, // The list of players in the party. - @SerializedName("players") + @SerializedName("players") val players: List, // The settings of the party - @SerializedName("settings") + @SerializedName("settings") val settings: Settings, ) diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Player.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Player.kt similarity index 96% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Player.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/models/Player.kt index 521c832d3..920028838 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Player.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Player.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.models +package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName import java.util.* diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt similarity index 96% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt index 96f07a806..33734cb12 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.models +package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 51613fab3..23c22d278 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -35,7 +35,7 @@ architectury { } loom { - accessWidenerPath.set(project(":common").loom.accessWidenerPath) + accessWidenerPath = project(":common").loom.accessWidenerPath forge { // This is required to convert the access wideners to the forge // format, access transformers. From 060a13bfaf7233179222c0ed4f74ec520f30ed0c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:07:26 -0500 Subject: [PATCH 03/30] Refactored the network, discord and http --- .../lambda/command/commands/DiscordCommand.kt | 24 +++ .../lambda/module/modules/client/Discord.kt | 175 +++++++++--------- .../lambda/module/modules/client/Network.kt | 51 ++--- .../network/api/v1/endpoints/CreateParty.kt | 8 +- .../network/api/v1/endpoints/DeleteParty.kt | 10 +- .../network/api/v1/endpoints/GetParty.kt | 4 +- .../network/api/v1/endpoints/JoinParty.kt | 8 +- .../network/api/v1/endpoints/LeaveParty.kt | 4 +- .../network/api/v1/endpoints/LinkDiscord.kt | 39 ++++ .../lambda/network/api/v1/endpoints/Login.kt | 10 +- .../network/api/v1/endpoints/UpdateParty.kt | 6 +- gradle.properties | 2 +- 12 files changed, 208 insertions(+), 133 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt diff --git a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt index 04beaf0f3..b83f7b32f 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt @@ -24,6 +24,8 @@ import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand import com.lambda.module.modules.client.Discord +import com.lambda.module.modules.client.Discord.rpc +import com.lambda.threading.runConcurrent import com.lambda.util.extension.CommandBuilder object DiscordCommand : LambdaCommand( @@ -39,5 +41,27 @@ object DiscordCommand : LambdaCommand( } } } + + required(literal("accept")) { + required(word("user")) { user -> + execute { + runConcurrent { rpc.activityManager.acceptJoinRequest(user().value()) } + } + } + } + + required(literal("refuse")) { + required(word("user")) { user -> + execute { + runConcurrent { rpc.activityManager.refuseJoinRequest(user().value()) } + } + } + } + + required(literal("create")) { + execute { + Discord.createParty() + } + } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 69562724e..f805e63e3 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -18,33 +18,30 @@ package com.lambda.module.modules.client import com.lambda.Lambda -import com.lambda.Lambda.LOG import com.lambda.context.SafeContext +import com.lambda.event.EventFlow import com.lambda.event.events.ConnectionEvent import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently import com.lambda.network.api.v1.models.Party import com.lambda.module.Module -import com.lambda.module.modules.client.Network.discordAuth -import com.lambda.module.modules.client.Network.isAuthenticated -import com.lambda.module.modules.client.Network.rpc -import com.lambda.module.modules.client.Network.apiAuth +import com.lambda.module.modules.client.Network.updateToken import com.lambda.module.tag.ModuleTag import com.lambda.network.api.v1.endpoints.createParty import com.lambda.network.api.v1.endpoints.editParty import com.lambda.network.api.v1.endpoints.joinParty import com.lambda.network.api.v1.endpoints.leaveParty +import com.lambda.network.api.v1.endpoints.linkDiscord import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe import com.lambda.util.Communication import com.lambda.util.Communication.toast +import com.lambda.util.Communication.warn import com.lambda.util.Nameable -import com.lambda.util.StringUtils.capitalize +import com.lambda.util.extension.dimensionName import dev.cbyrne.kdiscordipc.KDiscordIPC import dev.cbyrne.kdiscordipc.core.event.DiscordEvent -import dev.cbyrne.kdiscordipc.core.event.impl.ActivityJoinEvent -import dev.cbyrne.kdiscordipc.core.event.impl.ActivityJoinRequestEvent -import dev.cbyrne.kdiscordipc.core.event.impl.ErrorEvent import dev.cbyrne.kdiscordipc.core.event.impl.ReadyEvent +import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket import dev.cbyrne.kdiscordipc.data.activity.* import kotlinx.coroutines.delay import net.minecraft.entity.player.PlayerEntity @@ -64,43 +61,40 @@ object Discord : Module( private val line1Right by setting("Line 1 Right", LineInfo.USERNAME) { page == Page.General } private val line2Left by setting("Line 2 Left", LineInfo.DIMENSION) { page == Page.General } private val line2Right by setting("Line 2 Right", LineInfo.FPS) { page == Page.General } - private val confirmCoordinates by setting("Show Coordinates", false, description = "Confirm display the player coordinates") { page == Page.General } - private val confirmServer by setting("Show Server IP", false, description = "Confirm display the server IP") { page == Page.General } /* Party settings */ - private val enableParty by setting("Enable Party", true, description = "Allows you to create parties.") { page == Page.Party } // ToDo: Change this for create by default instead + private val createByDefault by setting("Create By Default", true, description = "Create parties on") { page == Page.Party } private val maxPlayers by setting("Max Players", 10, 2..20) { page == Page.Party }.onValueChange { _, _ -> if (player.isPartyOwner) edit() } // ToDo: Avoid spam requests - private var startup = System.currentTimeMillis() - private val dimensionRegex = Regex("""\b\w+_\w+\b""") // ToDo: Change this when combat is merged + val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) - private var ready: ReadyEvent? = null - private var currentParty: Party? = null + private var startup = System.currentTimeMillis() - private val isPartyInteractionAllowed: Boolean - get() = apiAuth != null && discordAuth != null + var discordAuth: AuthenticatePacket.Data? = null; private set + var currentParty: Party? = null; private set val PlayerEntity.isPartyOwner get() = uuid == currentParty?.leader?.uuid val PlayerEntity.isInParty - get() = currentParty?.players?.any { it.uuid == this.uuid } + get() = currentParty?.players?.any { it.uuid == uuid } init { + rpc.subscribe() + listenUnsafeConcurrently { // FixMe: We have to wait even though this is the last event until toSafe() != null // because of timing delay(3000) - runSafe { connect() } + runSafe { handleLoop() } } - // TODO: Exponential backoff up to 25 seconds to avoid being rate limited by discord - onEnable { connect() } - onDisable { disconnect() } + onEnable { runConcurrent { startDiscord(); handleLoop() } } + onDisable { stopDiscord() } } fun createParty() { - if (!isPartyInteractionAllowed) return + if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") val (party, error) = createParty(maxPlayers, true) if (error != null) toast("Failed to create a party: ${error.message}", Communication.LogLevel.WARN) @@ -108,9 +102,11 @@ object Discord : Module( currentParty = party } - // Join a party using the ID + /** + * Joins a new party with the invitation ID + */ fun join(id: String) { - if (!isPartyInteractionAllowed) return + if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") val (party, error) = joinParty(id) if (error != null) toast("Failed to join the party: ${error.message}", Communication.LogLevel.WARN) @@ -118,9 +114,11 @@ object Discord : Module( currentParty = party } - // Edit the current party if you are the owner - private fun edit() { - if (!isPartyInteractionAllowed) return + /** + * Triggers a party edit request if you are the owner + */ + fun edit() { + if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") val (party, error) = editParty(maxPlayers) if (error != null) toast("Failed to edit the party: ${error.message}", Communication.LogLevel.WARN) @@ -128,27 +126,69 @@ object Discord : Module( currentParty = party } - private fun SafeContext.connect() { - // FixMe: Race condition - runConcurrent { rpc.connect() } // ToDo: Duplicate rpc connection network and discord - runConcurrent { rpc.register() } - runConcurrent { - while (rpc.connected) { - update() - delay(delay) - } + /** + * Leaves the current party + */ + fun leave() { + if (discordAuth == null || currentParty == null) return toast("Can not interact with the api (are you offline?)") + + val (_, error) = leaveParty() + if (error != null) return toast("Failed to edit the party: ${error.message}", Communication.LogLevel.WARN) + + currentParty = null + } + + private suspend fun startDiscord() { + if (rpc.connected) return + + rpc.subscribe() + runConcurrent { rpc.connect() } // TODO: Create a function that will wait until x seconds has passed or if the connection is successful + delay(1000) + + val auth = rpc.applicationManager.authenticate() + val (authResp, error) = linkDiscord(discordToken = auth.accessToken) + if (error != null) { + warn(error.message.toString()) + return toast("Failed to link the discord account to the minecraft auth") } + + authResp?.let { updateToken(it) } + discordAuth = auth + } + + private fun stopDiscord() { + if (!rpc.connected) return + + rpc.disconnect() } - private fun disconnect() { - if (rpc.connected) { - LOG.info("Gracefully disconnecting from Discord RPC.") - leaveParty() - rpc.disconnect() + private fun KDiscordIPC.subscribe() { + // ToDO: Get party on join event + on { + subscribe(DiscordEvent.VoiceChannelSelect) + subscribe(DiscordEvent.VoiceStateCreate) + subscribe(DiscordEvent.VoiceStateUpdate) + subscribe(DiscordEvent.VoiceStateDelete) + subscribe(DiscordEvent.VoiceSettingsUpdate) + subscribe(DiscordEvent.VoiceConnectionStatus) + subscribe(DiscordEvent.SpeakingStart) + subscribe(DiscordEvent.SpeakingStop) + subscribe(DiscordEvent.ActivityJoin) + subscribe(DiscordEvent.ActivityJoinRequest) + subscribe(DiscordEvent.OverlayUpdate) + // subscribe(DiscordEvent.ActivitySpectate) // Unsupported } + } - ready = null - currentParty = null + private suspend fun SafeContext.handleLoop() { + if (createByDefault) createParty(maxPlayers) + + while (rpc.connected) { + update() + delay(delay) + } + + leave() } private suspend fun SafeContext.update() { @@ -161,7 +201,7 @@ object Discord : Module( largeImage("lambda", Lambda.VERSION) smallImage("https://mc-heads.net/avatar/${mc.gameProfile.id}/nohelm", mc.gameProfile.name) - if (isAuthenticated && party != null) { + if (party != null) { party(party.id.toString(), party.players.size, party.settings.maxPlayers) secrets(party.joinSecret) } else { @@ -172,33 +212,6 @@ object Discord : Module( } } - private suspend fun KDiscordIPC.register() { - on { - ready = this - - // Party features - subscribe(DiscordEvent.ActivityJoinRequest) - subscribe(DiscordEvent.ActivityJoin) - - if (enableParty) createParty() - } - - // Event when someone would like to join your party - on { - toast("The user ${data.userId} has invited you") - rpc.activityManager.acceptJoinRequest(data.userId) - } - - // Event when someone joins your party - on { - LOG.info("Someone has joined") - } - - on { - LOG.error("Discord RPC error: ${data.message}") - } - } - private enum class Page { General, Party } @@ -215,19 +228,9 @@ object Discord : Module( USERNAME({ mc.session.username }), HEALTH({ "${mc.player?.health ?: 0} HP" }), HUNGER({ "${mc.player?.hungerManager?.foodLevel ?: 0} Hunger" }), - DIMENSION({ - mc.world?.registryKey?.value?.path?.replace(dimensionRegex) { - it.value.split("_").joinToString(" ") { it.capitalize() } - } ?: "Unknown" - }), - COORDINATES({ - if (confirmCoordinates) "Coords: ${mc.player?.blockPos?.toShortString()}" - else "[Redacted]" - }), - SERVER({ - if (confirmServer) mc.currentServerEntry?.address ?: "Not Connected" - else "[Redacted]" - }), + DIMENSION({ dimensionName }), + COORDINATES({ "Coords: ${player.blockPos.toShortString()}" }), + SERVER({ mc.currentServerEntry?.address ?: "Not Connected" }), FPS({ "${mc.currentFps} FPS" }); } } 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 b4ba7f5f0..e16d1efa1 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 @@ -18,10 +18,10 @@ package com.lambda.module.modules.client import com.github.kittinunf.fuel.core.FuelManager -import com.lambda.Lambda +import com.lambda.Lambda.LOG import com.lambda.Lambda.mc -import com.lambda.event.EventFlow import com.lambda.event.events.ClientEvent +import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionRequest import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionResponse import com.lambda.event.listener.UnsafeListener.Companion.listenOnceUnsafe @@ -31,13 +31,9 @@ import com.lambda.network.api.v1.endpoints.login import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runConcurrent import com.lambda.util.Communication +import com.lambda.util.Communication.debug import com.lambda.util.Communication.toast -import com.lambda.util.Communication.warn -import dev.cbyrne.kdiscordipc.KDiscordIPC -import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket -import kotlinx.coroutines.delay import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler import net.minecraft.client.network.ServerAddress @@ -47,6 +43,7 @@ import net.minecraft.network.encryption.NetworkEncryptionUtils import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket import net.minecraft.text.Text import java.math.BigInteger +import java.sql.Time object Network : Module( name = "Network", @@ -55,40 +52,35 @@ object Network : Module( enabledByDefault = true, ) { var authServer by setting("Auth Server", "auth.lambda-client.org") - var apiUrl: String by setting("API Server", "https://api.lambda-client.org").onValueChange { _, to -> FuelManager.instance.basePath = "$to/api/$apiVersion" } - var apiVersion by setting("API Version", ApiVersion.V1).onValueChange { _, to -> FuelManager.instance.basePath = "$apiUrl/api/$to" } + var apiUrl: String by setting("API Server", "https://api.lambda-client.org") + var apiVersion by setting("API Version", ApiVersion.V1) - var discordAuth: AuthenticatePacket.Data? = null; private set var apiAuth: Authentication? = null; private set // TODO: Cache val accessToken: String get() = apiAuth?.accessToken ?: "" - val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) - - val isAuthenticated: Boolean - get() = discordAuth != null && apiAuth != null - private lateinit var serverId: String private lateinit var hash: String init { - FuelManager.instance.basePath = "${apiUrl}/api/${apiVersion}" - - listenUnsafe { - serverId = it.serverId - } + listenUnsafe { serverId = it.serverId } - listenOnceUnsafe { event -> - if (event.secretKey.isDestroyed) return@listenOnceUnsafe false + listenUnsafe { event -> + if (event.secretKey.isDestroyed) return@listenUnsafe hash = BigInteger( NetworkEncryptionUtils.computeServerId(serverId, event.publicKey, event.secretKey) ).toString(16) + } - val (authResponse, error) = login(discordAuth?.accessToken ?: "", mc.session.username, hash) + listenOnceUnsafe { + // If we log in right as the client responds to the encryption request, we start + // a race condition where the game server haven't acknowledged the packets + // and posted to the sessionserver api + val (authResponse, error) = login(mc.session.username, hash) if (error != null) { - toast("Unable to authenticate with the API", Communication.LogLevel.DEBUG) - return@listenOnceUnsafe false + LOG.debug("Unable to authenticate with the API: {}", error.errorData) + return@listenOnceUnsafe false } apiAuth = authResponse @@ -98,12 +90,7 @@ object Network : Module( } listenUnsafeConcurrently { - // TODO: add exponential backoff retries - runConcurrent { rpc.connect() } // TODO: Create a function that will wait until x seconds has passed or if the connection is successful - delay(1000) // hack - - discordAuth = rpc.applicationManager.authenticate() - + // ToDo: Check if player is online before connecting val addddd = ServerAddress.parse(authServer) val connection = ClientConnection(CLIENTBOUND) val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) @@ -119,6 +106,8 @@ object Network : Module( } } + internal fun updateToken(auth: Authentication) { apiAuth = auth } + enum class ApiVersion(val value: String) { // We can use @Deprecated("Not supported") to remove old API versions in the future V1("v1"), diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt index 8f4c98597..5c1af444c 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt @@ -18,9 +18,14 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.Headers import com.github.kittinunf.fuel.core.extensions.authentication +import com.github.kittinunf.fuel.core.extensions.jsonBody +import com.github.kittinunf.fuel.gson.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party fun createParty( @@ -33,7 +38,8 @@ fun createParty( // example: true public: Boolean = true, ) = - Fuel.post("/party/create", listOf("max_players" to maxPlayers, "public" to public)) + Fuel.post("${apiUrl}/api/${apiVersion.value}/party/create") + .jsonBody("""{ "max_players": $maxPlayers }""") .authentication() .bearer(Network.accessToken) .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt index b01e4dd07..468dcbff1 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt @@ -19,15 +19,15 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication +import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party -fun deleteParty( - endpoint: String, - version: String, -) = - Fuel.delete("$endpoint/api/$version/party/delete") +fun deleteParty() = + Fuel.delete("${apiUrl}/api/${apiVersion.value}/party/delete") .authentication() .bearer(Network.accessToken) .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt index a2badb7c6..ba845950d 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt @@ -21,10 +21,12 @@ import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party fun getParty() = - Fuel.get("/party") + Fuel.get("${apiUrl}/api/${apiVersion.value}/party") .authentication() .bearer(Network.accessToken) .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt index 01582dd82..12e56e757 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt @@ -18,9 +18,14 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.Headers import com.github.kittinunf.fuel.core.extensions.authentication +import com.github.kittinunf.fuel.core.extensions.jsonBody +import com.github.kittinunf.fuel.gson.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party fun joinParty( @@ -28,7 +33,8 @@ fun joinParty( // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" partyId: String, ) = - Fuel.put("/party/join", listOf("id" to partyId)) + Fuel.put("${apiUrl}/api/${apiVersion.value}/party/join", listOf("id" to partyId)) + .jsonBody("""{ "id": "$partyId" }""") .authentication() .bearer(Network.accessToken) .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt index 950723e28..b265cac96 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt @@ -21,10 +21,12 @@ import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party fun leaveParty() = - Fuel.put("/party/leave") + Fuel.put("${apiUrl}/api/${apiVersion.value}/party/leave") .authentication() .bearer(Network.accessToken) .responseObject().third 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 new file mode 100644 index 000000000..014928c2c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt @@ -0,0 +1,39 @@ +/* + * 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.Headers +import com.github.kittinunf.fuel.core.extensions.authentication +import com.github.kittinunf.fuel.core.extensions.jsonBody +import com.github.kittinunf.fuel.gson.responseObject +import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion +import com.lambda.network.api.v1.models.Authentication + +fun linkDiscord( + // The player's Discord token. + // example: OTk1MTU1NzcyMzYxMTQ2NDM4 + discordToken: String, +) = + Fuel.post("${apiUrl}/api/${apiVersion.value}/link/discord") + .jsonBody("""{ "token": "$discordToken" }""") + .authentication() + .bearer(Network.accessToken) + .responseObject().third 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 7125b0c6e..e6d411fce 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 @@ -18,14 +18,13 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Authentication fun login( - // The player's Discord token. - // example: OTk1MTU1NzcyMzYxMTQ2NDM4 - discordToken: String, - // The player's username. // example: "Notch" username: String, @@ -34,5 +33,6 @@ fun login( // example: 069a79f444e94726a5befca90e38aaf5 hash: String, ) = - Fuel.post("/login", listOf("token" to discordToken, "username" to username, "hash" to hash)) + Fuel.post("${apiUrl}/api/${apiVersion.value}/login") + .jsonBody("""{ "username": "$username", "hash": "$hash" }""") .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt index dec5f9b1b..8e37c5c4e 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt @@ -19,8 +19,11 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication +import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party fun editParty( @@ -33,7 +36,8 @@ fun editParty( // example: true // public: Boolean = true, ) = - Fuel.patch("/party/edit", listOf("max_players" to maxPlayers)) + Fuel.patch("${apiUrl}/api/${apiVersion.value}/party/edit") + .jsonBody("""{ "max_players": $maxPlayers }""") .authentication() .bearer(Network.accessToken) .responseObject().third diff --git a/gradle.properties b/gradle.properties index dd7d3f16f..ec54135cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,7 +31,7 @@ kotlinVersion=2.0.20 kotlinxCoroutinesVersion=1.9.0-RC javaVersion=17 baritoneVersion=1.10.2 -discordIPCVersion=7ab2e77312 +discordIPCVersion=8edf2dbeda fuelVersion=2.3.1 # Fabric https://fabricmc.net/develop/ From 9b8a9a9726a1fd3abf6523431003176933e379bf Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:43:03 -0500 Subject: [PATCH 04/30] Refactored the discord module --- .../lambda/mixin/world/ClientWorldMixin.java | 11 +++ common/src/main/kotlin/com/lambda/Lambda.kt | 43 ++++++++ .../lambda/command/commands/DiscordCommand.kt | 34 +++++-- .../com/lambda/event/events/WorldEvent.kt | 4 + .../lambda/module/modules/client/Discord.kt | 97 +++++++++---------- .../lambda/module/modules/client/Network.kt | 21 ++-- .../network/api/v1/endpoints/CreateParty.kt | 15 +-- .../network/api/v1/endpoints/LinkDiscord.kt | 1 - .../{UpdateParty.kt => PartyUpdates.kt} | 43 ++++---- .../network/api/v1/models/Authentication.kt | 23 ++++- 10 files changed, 188 insertions(+), 104 deletions(-) rename common/src/main/kotlin/com/lambda/network/api/v1/endpoints/{UpdateParty.kt => PartyUpdates.kt} (53%) diff --git a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java index 877548369..296cffe53 100644 --- a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java +++ b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java @@ -23,8 +23,12 @@ import com.lambda.module.modules.render.WorldColors; import com.lambda.util.math.ColorKt; import net.minecraft.block.BlockState; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.render.WorldRenderer; import net.minecraft.client.world.ClientWorld; import net.minecraft.entity.Entity; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import org.spongepowered.asm.mixin.Mixin; @@ -33,8 +37,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.function.Supplier; + @Mixin(ClientWorld.class) public class ClientWorldMixin { + @Inject(method = "(Lnet/minecraft/client/network/ClientPlayNetworkHandler;Lnet/minecraft/client/world/ClientWorld$Properties;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/registry/entry/RegistryEntry;IILjava/util/function/Supplier;Lnet/minecraft/client/render/WorldRenderer;ZJ)V", at = @At("TAIL")) + void constructorMixin(ClientPlayNetworkHandler networkHandler, ClientWorld.Properties properties, RegistryKey registryRef, RegistryEntry dimensionTypeEntry, int loadDistance, int simulationDistance, Supplier profiler, WorldRenderer worldRenderer, boolean debugWorld, long seed, CallbackInfo ci) { + EventFlow.post(new WorldEvent.Join()); + } + @Inject(method = "addEntity", at = @At("HEAD"), cancellable = true) private void onAddEntity(Entity entity, CallbackInfo ci) { if (EventFlow.post(new EntityEvent.EntitySpawn(entity)).isCanceled()) ci.cancel(); diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index a88c0b570..0869b8af2 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -17,6 +17,10 @@ package com.lambda +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.await +import com.github.kittinunf.fuel.core.awaitResponse +import com.github.kittinunf.fuel.core.awaitUnit import com.google.gson.Gson import com.google.gson.GsonBuilder import com.lambda.config.serializer.* @@ -27,6 +31,7 @@ import com.lambda.core.Loader import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.TagWindow import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runConcurrent import com.lambda.util.KeyCode import com.mojang.authlib.GameProfile import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall @@ -38,7 +43,12 @@ import net.minecraft.util.math.BlockPos import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import java.awt.Color +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse import java.util.* +import java.util.concurrent.CountDownLatch object Lambda { @@ -70,6 +80,39 @@ object Lambda { .create() fun initialize(block: (Long) -> Unit) { + runConcurrent { + // Create an HttpClient + val client = HttpClient.newHttpClient() + + // Define the URI for the SSE stream + val uri = URI.create("http://localhost:8080/api/v1/party/listen") + + // Create an HttpRequest for the SSE stream + val request = HttpRequest.newBuilder() + .uri(uri) + .header("Accept", "text/event-stream") + .build() + + // Create a CountDownLatch to wait for the events + val latch = CountDownLatch(1) + + // Send the request and handle the response asynchronously + client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) + .thenAccept { response -> + println("Connected to SSE stream") + response.body().forEach { line -> + if (line.startsWith("data:")) { + val data = line.substring(5).trim() + println("Received event data: $data") + } + } + latch.countDown() + } + + // Wait until the response is received and handled + latch.await() + } + recordRenderCall { block(Loader.initialize()) } diff --git a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt index b83f7b32f..8401d518f 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt @@ -23,21 +23,43 @@ import com.lambda.brigadier.argument.word import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand -import com.lambda.module.modules.client.Discord +import com.lambda.module.modules.client.Discord.partyCreate +import com.lambda.module.modules.client.Discord.partyJoin +import com.lambda.module.modules.client.Discord.partyLeave import com.lambda.module.modules.client.Discord.rpc +import com.lambda.network.api.v1.endpoints.leaveParty import com.lambda.threading.runConcurrent +import com.lambda.threading.runSafe import com.lambda.util.extension.CommandBuilder object DiscordCommand : LambdaCommand( name = "discord", description = "Discord Rich Presence commands", - usage = "rpc " + usage = "rpc " ) { override fun CommandBuilder.create() { + required(literal("create")) { + execute { + runSafe { partyCreate() } + } + } + + required(literal("leave")) { + execute { + runSafe { partyLeave() } + } + } + + required(literal("delete")) { + execute { + runSafe { partyCreate() } + } + } + required(literal("join")) { required(word("id")) { id -> execute { - Discord.join(id().value()) + runSafe { partyJoin(id().value()) } } } } @@ -57,11 +79,5 @@ object DiscordCommand : LambdaCommand( } } } - - required(literal("create")) { - execute { - Discord.createParty() - } - } } } diff --git a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt index 202ab03f9..f90efb085 100644 --- a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt @@ -36,6 +36,10 @@ import net.minecraft.world.chunk.WorldChunk * occurrences in the game world. */ sealed class WorldEvent { + // ToDo: Add doc + // Represents the player joining the client world + class Join() : Event + /** * Represents an event specific to chunk operations within the world. * diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index f805e63e3..08f9fddd1 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -20,20 +20,22 @@ package com.lambda.module.modules.client import com.lambda.Lambda import com.lambda.context.SafeContext import com.lambda.event.EventFlow -import com.lambda.event.events.ConnectionEvent -import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently +import com.lambda.event.events.RenderEvent +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.event.listener.SafeListener.Companion.listenConcurrently import com.lambda.network.api.v1.models.Party import com.lambda.module.Module +import com.lambda.module.modules.client.Network.isDiscordLinked import com.lambda.module.modules.client.Network.updateToken import com.lambda.module.tag.ModuleTag import com.lambda.network.api.v1.endpoints.createParty -import com.lambda.network.api.v1.endpoints.editParty +import com.lambda.network.api.v1.endpoints.deleteParty import com.lambda.network.api.v1.endpoints.joinParty import com.lambda.network.api.v1.endpoints.leaveParty import com.lambda.network.api.v1.endpoints.linkDiscord +import com.lambda.network.api.v1.endpoints.partyUpdates import com.lambda.threading.runConcurrent -import com.lambda.threading.runSafe -import com.lambda.util.Communication import com.lambda.util.Communication.toast import com.lambda.util.Communication.warn import com.lambda.util.Nameable @@ -64,7 +66,6 @@ object Discord : Module( /* Party settings */ private val createByDefault by setting("Create By Default", true, description = "Create parties on") { page == Page.Party } - private val maxPlayers by setting("Max Players", 10, 2..20) { page == Page.Party }.onValueChange { _, _ -> if (player.isPartyOwner) edit() } // ToDo: Avoid spam requests val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) @@ -76,72 +77,72 @@ object Discord : Module( val PlayerEntity.isPartyOwner get() = uuid == currentParty?.leader?.uuid - val PlayerEntity.isInParty - get() = currentParty?.players?.any { it.uuid == uuid } + val PlayerEntity.isInParty: Boolean + get() = currentParty?.players?.any { it.uuid == uuid } ?: false init { rpc.subscribe() - listenUnsafeConcurrently { - // FixMe: We have to wait even though this is the last event until toSafe() != null - // because of timing - delay(3000) - runSafe { handleLoop() } - } - - onEnable { runConcurrent { startDiscord(); handleLoop() } } - onDisable { stopDiscord() } - } - - fun createParty() { - if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") + // ToDo: Nametag for friends when ref/ui is merged + // listen() - val (party, error) = createParty(maxPlayers, true) - if (error != null) toast("Failed to create a party: ${error.message}", Communication.LogLevel.WARN) + listenConcurrently { + // If the player is in a party and this most likely means that the `onEnable` + // block ran and is already handling the activity + if (player.isInParty) return@listenConcurrently + handleLoop() + } - currentParty = party + onEnable { runConcurrent { start(); handleLoop() } } + onDisable { stop() } } /** - * Joins a new party with the invitation ID + * Creates a new party, leaves or delete the current party if there is one */ - fun join(id: String) { - if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") + fun SafeContext.partyCreate() { + if (!isDiscordLinked) return warn("You did not link your discord account") + if (!player.isInParty) { + if (player.isPartyOwner) deleteParty() else leaveParty() + return + } - val (party, error) = joinParty(id) - if (error != null) toast("Failed to join the party: ${error.message}", Communication.LogLevel.WARN) + val (party, error) = createParty() + if (error != null) warn("Failed to create a party: ${error.errorData}") currentParty = party + partyUpdates { currentParty = it } } /** - * Triggers a party edit request if you are the owner + * Joins a new party with the invitation ID */ - fun edit() { - if (discordAuth == null) return toast("Can not interact with the api (are you offline?)") + fun SafeContext.partyJoin(id: String) { + if (!isDiscordLinked) return warn("You did not link your discord account") - val (party, error) = editParty(maxPlayers) - if (error != null) toast("Failed to edit the party: ${error.message}", Communication.LogLevel.WARN) + val (party, error) = joinParty(id) + if (error != null) warn("Failed to join the party: ${error.errorData}") currentParty = party + partyUpdates { currentParty = it } } /** * Leaves the current party */ - fun leave() { - if (discordAuth == null || currentParty == null) return toast("Can not interact with the api (are you offline?)") + fun SafeContext.partyLeave() { + if (!isDiscordLinked) return warn("You did not link your discord account") + if (!player.isInParty) return warn("You are not in a party") val (_, error) = leaveParty() - if (error != null) return toast("Failed to edit the party: ${error.message}", Communication.LogLevel.WARN) + if (error != null) return warn("Failed to leave the party: ${error.errorData}") currentParty = null } - private suspend fun startDiscord() { + private suspend fun start() { if (rpc.connected) return - rpc.subscribe() runConcurrent { rpc.connect() } // TODO: Create a function that will wait until x seconds has passed or if the connection is successful delay(1000) @@ -152,18 +153,15 @@ object Discord : Module( return toast("Failed to link the discord account to the minecraft auth") } - authResp?.let { updateToken(it) } + updateToken(authResp) discordAuth = auth } - private fun stopDiscord() { - if (!rpc.connected) return - - rpc.disconnect() + private fun stop() { + if (rpc.connected) rpc.disconnect() } private fun KDiscordIPC.subscribe() { - // ToDO: Get party on join event on { subscribe(DiscordEvent.VoiceChannelSelect) subscribe(DiscordEvent.VoiceStateCreate) @@ -181,14 +179,17 @@ object Discord : Module( } private suspend fun SafeContext.handleLoop() { - if (createByDefault) createParty(maxPlayers) + if (isDiscordLinked) { + if (createByDefault) createParty() + partyUpdates { currentParty = it } + } while (rpc.connected) { update() delay(delay) } - leave() + if (isDiscordLinked) leaveParty() } private suspend fun SafeContext.update() { @@ -229,8 +230,6 @@ object Discord : Module( HEALTH({ "${mc.player?.health ?: 0} HP" }), HUNGER({ "${mc.player?.hungerManager?.foodLevel ?: 0} Hunger" }), DIMENSION({ dimensionName }), - COORDINATES({ "Coords: ${player.blockPos.toShortString()}" }), - SERVER({ mc.currentServerEntry?.address ?: "Not Connected" }), FPS({ "${mc.currentFps} FPS" }); } } 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 e16d1efa1..aeab65eed 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 @@ -17,9 +17,9 @@ package com.lambda.module.modules.client -import com.github.kittinunf.fuel.core.FuelManager import com.lambda.Lambda.LOG import com.lambda.Lambda.mc +import com.lambda.context.SafeContext import com.lambda.event.events.ClientEvent import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionRequest @@ -31,9 +31,7 @@ import com.lambda.network.api.v1.endpoints.login import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.util.Communication -import com.lambda.util.Communication.debug -import com.lambda.util.Communication.toast +import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler import net.minecraft.client.network.ServerAddress @@ -43,7 +41,6 @@ import net.minecraft.network.encryption.NetworkEncryptionUtils import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket import net.minecraft.text.Text import java.math.BigInteger -import java.sql.Time object Network : Module( name = "Network", @@ -51,14 +48,17 @@ object Network : Module( defaultTags = setOf(ModuleTag.CLIENT), enabledByDefault = true, ) { - var authServer by setting("Auth Server", "auth.lambda-client.org") - var apiUrl: String by setting("API Server", "https://api.lambda-client.org") - var apiVersion by setting("API Version", ApiVersion.V1) + val authServer by setting("Auth Server", "auth.lambda-client.org") + val apiUrl: String by setting("API Server", "https://api.lambda-client.org") + val apiVersion by setting("API Version", ApiVersion.V1) var apiAuth: Authentication? = null; private set // TODO: Cache val accessToken: String get() = apiAuth?.accessToken ?: "" + val SafeContext.isDiscordLinked: Boolean + get() = apiAuth?.decoded?.data?.discordId != null + private lateinit var serverId: String private lateinit var hash: String @@ -90,7 +90,8 @@ object Network : Module( } listenUnsafeConcurrently { - // ToDo: Check if player is online before connecting + if (mc.gameProfile.isOffline) return@listenUnsafeConcurrently + val addddd = ServerAddress.parse(authServer) val connection = ClientConnection(CLIENTBOUND) val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) @@ -106,7 +107,7 @@ object Network : Module( } } - internal fun updateToken(auth: Authentication) { apiAuth = auth } + internal fun updateToken(auth: Authentication?) { apiAuth = auth } enum class ApiVersion(val value: String) { // We can use @Deprecated("Not supported") to remove old API versions in the future diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt index 5c1af444c..415a1e10f 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt @@ -18,28 +18,15 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.Headers import com.github.kittinunf.fuel.core.extensions.authentication -import com.github.kittinunf.fuel.core.extensions.jsonBody -import com.github.kittinunf.fuel.gson.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party -fun createParty( - // The maximum number of players in the party. - // example: 10 - maxPlayers: Int = 10, - - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - public: Boolean = true, -) = +fun createParty() = Fuel.post("${apiUrl}/api/${apiVersion.value}/party/create") - .jsonBody("""{ "max_players": $maxPlayers }""") .authentication() .bearer(Network.accessToken) .responseObject().third 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 014928c2c..e357a390a 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 @@ -18,7 +18,6 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.Headers import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt similarity index 53% rename from common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt rename to common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt index 8e37c5c4e..ccb4aaf86 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/UpdateParty.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Lambda + * 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 @@ -17,27 +17,30 @@ package com.lambda.network.api.v1.endpoints -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.extensions.authentication -import com.github.kittinunf.fuel.core.extensions.jsonBody -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network +import com.lambda.Lambda import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse -fun editParty( - // The maximum number of players in the party. - // example: 10 - maxPlayers: Int = 10, +// Waiting for https://github.com/kittinunf/fuel/issues/989 before changing this +fun partyUpdates(block: (Party) -> Unit) { + HttpClient.newHttpClient().sendAsync( + HttpRequest.newBuilder() + .uri(URI.create("${apiUrl}/api/${apiVersion.value}/party/listen")) + .header("Accept", "text/event-stream") + .build(), + HttpResponse.BodyHandlers.ofLines() + ).thenAccept { response -> + response.body().forEach { + if (!it.startsWith("data:")) return@forEach - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - // public: Boolean = true, -) = - Fuel.patch("${apiUrl}/api/${apiVersion.value}/party/edit") - .jsonBody("""{ "max_players": $maxPlayers }""") - .authentication() - .bearer(Network.accessToken) - .responseObject().third + val data = it.substring(5).trim() + val party = Lambda.gson.fromJson(data, Party::class.java) + block(party) + } + }.join() +} diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt index d8886535d..c15b40543 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt @@ -18,6 +18,10 @@ package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName +import com.lambda.Lambda +import java.time.Instant +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi data class Authentication( // The access token to use for the API @@ -34,4 +38,21 @@ data class Authentication( // example: Bearer @SerializedName("token_type") val tokenType: String, -) +) { + @OptIn(ExperimentalEncodingApi::class) + val decoded = Lambda.gson.fromJson(Base64.decode(accessToken).toString(), Payload::class.java) + + data class Payload( + @SerializedName("nbf") + val notBefore: Instant, + + @SerializedName("iat") + val issuedAt: Instant, + + @SerializedName("exp") + val expirationDate: Instant, + + @SerializedName("data") + val data: Player, + ) +} From cfa2e3f9bedfd8d35a781d7c34d87078f9fd36dd Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:59:32 -0500 Subject: [PATCH 05/30] Fixed world join event --- .../mixin/network/ClientPlayNetworkHandlerMixin.java | 7 +++++++ .../main/java/com/lambda/mixin/world/ClientWorldMixin.java | 5 ----- .../src/main/kotlin/com/lambda/event/events/WorldEvent.kt | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java index 3db1ca635..7fd8cfbdc 100644 --- a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java +++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java @@ -19,7 +19,9 @@ import com.lambda.event.EventFlow; import com.lambda.event.events.InventoryEvent; +import com.lambda.event.events.WorldEvent; import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket; import org.spongepowered.asm.mixin.Mixin; @@ -29,6 +31,11 @@ @Mixin(ClientPlayNetworkHandler.class) public class ClientPlayNetworkHandlerMixin { + @Inject(method = "onGameJoin(Lnet/minecraft/network/packet/s2c/play/GameJoinS2CPacket;)V", at = @At("TAIL")) + void injectJoinPacket(GameJoinS2CPacket packet, CallbackInfo ci) { + EventFlow.post(new WorldEvent.Join()); + } + @Inject(method = "onUpdateSelectedSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V", shift = At.Shift.AFTER), cancellable = true) private void onUpdateSelectedSlot(UpdateSelectedSlotS2CPacket packet, CallbackInfo ci) { if (EventFlow.post(new InventoryEvent.HotbarSlot.Sync(packet.getSlot())).isCanceled()) ci.cancel(); diff --git a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java index 296cffe53..37d039efe 100644 --- a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java +++ b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java @@ -41,11 +41,6 @@ @Mixin(ClientWorld.class) public class ClientWorldMixin { - @Inject(method = "(Lnet/minecraft/client/network/ClientPlayNetworkHandler;Lnet/minecraft/client/world/ClientWorld$Properties;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/registry/entry/RegistryEntry;IILjava/util/function/Supplier;Lnet/minecraft/client/render/WorldRenderer;ZJ)V", at = @At("TAIL")) - void constructorMixin(ClientPlayNetworkHandler networkHandler, ClientWorld.Properties properties, RegistryKey registryRef, RegistryEntry dimensionTypeEntry, int loadDistance, int simulationDistance, Supplier profiler, WorldRenderer worldRenderer, boolean debugWorld, long seed, CallbackInfo ci) { - EventFlow.post(new WorldEvent.Join()); - } - @Inject(method = "addEntity", at = @At("HEAD"), cancellable = true) private void onAddEntity(Entity entity, CallbackInfo ci) { if (EventFlow.post(new EntityEvent.EntitySpawn(entity)).isCanceled()) ci.cancel(); diff --git a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt index f90efb085..6e9f60d7b 100644 --- a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt @@ -36,8 +36,8 @@ import net.minecraft.world.chunk.WorldChunk * occurrences in the game world. */ sealed class WorldEvent { - // ToDo: Add doc - // Represents the player joining the client world + // ToDo: Add doc and determine if there's a better place for this event + // Represents the player joining the world class Join() : Event /** From f049fabc8a591e72cd024a6175323e5854d05efb Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:06:51 -0500 Subject: [PATCH 06/30] Update common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../main/kotlin/com/lambda/module/modules/client/Discord.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 08f9fddd1..8b6c922e8 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -102,10 +102,11 @@ object Discord : Module( */ fun SafeContext.partyCreate() { if (!isDiscordLinked) return warn("You did not link your discord account") - if (!player.isInParty) { + if (player.isInParty) { if (player.isPartyOwner) deleteParty() else leaveParty() return } + } val (party, error) = createParty() if (error != null) warn("Failed to create a party: ${error.errorData}") From 8608e2578a2b364ea7554bac16900bf3a4f54983 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:10:31 -0500 Subject: [PATCH 07/30] Fixed discord delete logic --- .../com/lambda/command/commands/DiscordCommand.kt | 4 +++- .../com/lambda/module/modules/client/Discord.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt index 8401d518f..c5f893358 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt @@ -24,9 +24,11 @@ import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand import com.lambda.module.modules.client.Discord.partyCreate +import com.lambda.module.modules.client.Discord.partyDelete import com.lambda.module.modules.client.Discord.partyJoin import com.lambda.module.modules.client.Discord.partyLeave import com.lambda.module.modules.client.Discord.rpc +import com.lambda.network.api.v1.endpoints.deleteParty import com.lambda.network.api.v1.endpoints.leaveParty import com.lambda.threading.runConcurrent import com.lambda.threading.runSafe @@ -52,7 +54,7 @@ object DiscordCommand : LambdaCommand( required(literal("delete")) { execute { - runSafe { partyCreate() } + runSafe { partyDelete() } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 08f9fddd1..34a780edd 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -140,6 +140,20 @@ object Discord : Module( currentParty = null } + /** + * Deletes the current party + */ + fun SafeContext.partyDelete() { + if (!isDiscordLinked) return warn("You did not link your discord account") + if (!player.isInParty) return warn("You are not in a party") + + val (_, error) = deleteParty() + if (error != null) return warn("Failed to delete the party: ${error.errorData}") + + currentParty = null + + } + private suspend fun start() { if (rpc.connected) return From 7aae60e6f5141af4ca14840b1d602ac09dfbe284 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:11:43 -0500 Subject: [PATCH 08/30] Removed extra bracket --- .../src/main/kotlin/com/lambda/module/modules/client/Discord.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 3ddce0d0a..2426ba317 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -106,7 +106,6 @@ object Discord : Module( if (player.isPartyOwner) deleteParty() else leaveParty() return } - } val (party, error) = createParty() if (error != null) warn("Failed to create a party: ${error.errorData}") From 29b0709d63bcb5cc03582e81576dec275631f2b8 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:16:17 -0500 Subject: [PATCH 09/30] Fixed code flow on failure --- .../main/kotlin/com/lambda/module/modules/client/Discord.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 2426ba317..88dcf4d5e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -108,7 +108,7 @@ object Discord : Module( } val (party, error) = createParty() - if (error != null) warn("Failed to create a party: ${error.errorData}") + if (error != null) return warn("Failed to create a party: ${error.errorData}") currentParty = party partyUpdates { currentParty = it } @@ -121,7 +121,7 @@ object Discord : Module( if (!isDiscordLinked) return warn("You did not link your discord account") val (party, error) = joinParty(id) - if (error != null) warn("Failed to join the party: ${error.errorData}") + if (error != null) return warn("Failed to join the party: ${error.errorData}") currentParty = party partyUpdates { currentParty = it } From e757afca5e3ba1751056fa49461c9d679c0ee451 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:05:19 -0500 Subject: [PATCH 10/30] Fixed stupid gson --- common/src/main/kotlin/com/lambda/Lambda.kt | 4 ---- .../com/lambda/module/modules/client/Network.kt | 11 ++++++++--- .../network/api/v1/models/Authentication.kt | 15 ++++----------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index 0869b8af2..084aeb8eb 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -17,10 +17,6 @@ package com.lambda -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.core.await -import com.github.kittinunf.fuel.core.awaitResponse -import com.github.kittinunf.fuel.core.awaitUnit import com.google.gson.Gson import com.google.gson.GsonBuilder import com.lambda.config.serializer.* 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 aeab65eed..2a75debe9 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 @@ -18,6 +18,7 @@ package com.lambda.module.modules.client import com.lambda.Lambda.LOG +import com.lambda.Lambda.gson import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.event.events.ClientEvent @@ -31,6 +32,7 @@ import com.lambda.network.api.v1.endpoints.login import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.network.api.v1.models.Authentication.Data import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler @@ -41,6 +43,7 @@ import net.minecraft.network.encryption.NetworkEncryptionUtils import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket import net.minecraft.text.Text import java.math.BigInteger +import java.util.* object Network : Module( name = "Network", @@ -49,15 +52,16 @@ object Network : Module( enabledByDefault = true, ) { val authServer by setting("Auth Server", "auth.lambda-client.org") - val apiUrl: String by setting("API Server", "https://api.lambda-client.org") + val apiUrl by setting("API Server", "https://api.lambda-client.org") val apiVersion by setting("API Version", ApiVersion.V1) var apiAuth: Authentication? = null; private set // TODO: Cache + var deserialized: Data? = null; private set // gson is too stupid val accessToken: String get() = apiAuth?.accessToken ?: "" val SafeContext.isDiscordLinked: Boolean - get() = apiAuth?.decoded?.data?.discordId != null + get() = deserialized?.data?.discordId != null private lateinit var serverId: String private lateinit var hash: String @@ -79,11 +83,12 @@ object Network : Module( // and posted to the sessionserver api val (authResponse, error) = login(mc.session.username, hash) if (error != null) { - LOG.debug("Unable to authenticate with the API: {}", error.errorData) + LOG.debug("Unable to authenticate with the API: ${error.message}") return@listenOnceUnsafe false } apiAuth = authResponse + deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) // Destroy the listener true diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt index c15b40543..7dfb3a6a8 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Authentication.kt @@ -18,10 +18,6 @@ package com.lambda.network.api.v1.models import com.google.gson.annotations.SerializedName -import com.lambda.Lambda -import java.time.Instant -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi data class Authentication( // The access token to use for the API @@ -39,18 +35,15 @@ data class Authentication( @SerializedName("token_type") val tokenType: String, ) { - @OptIn(ExperimentalEncodingApi::class) - val decoded = Lambda.gson.fromJson(Base64.decode(accessToken).toString(), Payload::class.java) - - data class Payload( + data class Data( @SerializedName("nbf") - val notBefore: Instant, + val notBefore: Long, @SerializedName("iat") - val issuedAt: Instant, + val issuedAt: Long, @SerializedName("exp") - val expirationDate: Instant, + val expirationDate: Long, @SerializedName("data") val data: Player, From a2f9a66880da342da01f754853e8795e2c2fbaf1 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:03:37 -0500 Subject: [PATCH 11/30] Updated logic and fixed bugs --- .../lambda/module/modules/client/Discord.kt | 14 ++++--- .../lambda/module/modules/client/Network.kt | 38 +++++++++++++------ .../network/api/v1/endpoints/PartyUpdates.kt | 36 ++++++++++-------- .../com/lambda/network/api/v1/models/Party.kt | 4 -- .../lambda/network/api/v1/models/Settings.kt | 33 ---------------- 5 files changed, 55 insertions(+), 70 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 88dcf4d5e..1568c5ccc 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -89,12 +89,15 @@ object Discord : Module( listenConcurrently { // If the player is in a party and this most likely means that the `onEnable` // block ran and is already handling the activity - if (player.isInParty) return@listenConcurrently + if (rpc.connected && player.isInParty) return@listenConcurrently + + start() handleLoop() + stop() } - onEnable { runConcurrent { start(); handleLoop() } } onDisable { stop() } + onEnable { runConcurrent { start(); handleLoop() } } } /** @@ -104,11 +107,10 @@ object Discord : Module( if (!isDiscordLinked) return warn("You did not link your discord account") if (player.isInParty) { if (player.isPartyOwner) deleteParty() else leaveParty() - return } val (party, error) = createParty() - if (error != null) return warn("Failed to create a party: ${error.errorData}") + if (error != null) return warn("Failed to create a party: ${error.exception}") currentParty = party partyUpdates { currentParty = it } @@ -148,7 +150,7 @@ object Discord : Module( if (!player.isInParty) return warn("You are not in a party") val (_, error) = deleteParty() - if (error != null) return warn("Failed to delete the party: ${error.errorData}") + if (error != null) return warn("Failed to delete the party: ${error.exception}") currentParty = null @@ -217,7 +219,7 @@ object Discord : Module( smallImage("https://mc-heads.net/avatar/${mc.gameProfile.id}/nohelm", mc.gameProfile.name) if (party != null) { - party(party.id.toString(), party.players.size, party.settings.maxPlayers) + party(party.id.toString(), party.players.size, 20) // Placeholder while secrets(party.joinSecret) } else { button("Download", "https://github.com/lambda-client/lambda") 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 2a75debe9..cafe2c744 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 @@ -17,6 +17,7 @@ package com.lambda.module.modules.client +import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.Lambda.gson import com.lambda.Lambda.mc @@ -33,6 +34,7 @@ import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.network.api.v1.models.Authentication.Data +import com.lambda.util.Communication.info import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler @@ -78,38 +80,50 @@ object Network : Module( } listenOnceUnsafe { + if (!::hash.isInitialized) { + if (!authenticate()) return@listenOnceUnsafe false + } + // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets // and posted to the sessionserver api val (authResponse, error) = login(mc.session.username, hash) if (error != null) { - LOG.debug("Unable to authenticate with the API: ${error.message}") + LOG.debug("Unable to authenticate: ${error.message}") return@listenOnceUnsafe false } apiAuth = authResponse deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) - // Destroy the listener + LOG.info("Successfully authenticated") true } - listenUnsafeConcurrently { - if (mc.gameProfile.isOffline) return@listenUnsafeConcurrently + listenUnsafeConcurrently { authenticate() } + } + + private fun authenticate(): Boolean { + if (mc.gameProfile.isOffline) return true - val addddd = ServerAddress.parse(authServer) - val connection = ClientConnection(CLIENTBOUND) - val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) - .map { it.inetSocketAddress }.get() + val addddd = ServerAddress.parse(authServer) + val connection = ClientConnection(CLIENTBOUND) + val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) + .map { it.inetSocketAddress }.get() + runCatching { ClientConnection.connect(addr, mc.options.shouldUseNativeTransport(), connection) .syncUninterruptibly() + }.onFailure { + return false + } - val handler = ClientLoginNetworkHandler(connection, mc, null, null, false, null) { Text.empty() } + val handler = ClientLoginNetworkHandler(connection, mc, null, null, false, null) { Text.empty() } - connection.connect(addr.hostName, addr.port, handler) - connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) - } + connection.connect(addr.hostName, addr.port, handler) + connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) + + return true } internal fun updateToken(auth: Authentication?) { apiAuth = auth } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt index ccb4aaf86..53454a5d3 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt @@ -18,29 +18,35 @@ package com.lambda.network.api.v1.endpoints import com.lambda.Lambda +import com.lambda.module.modules.client.Network.accessToken import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Party +import com.lambda.threading.runConcurrent import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse // Waiting for https://github.com/kittinunf/fuel/issues/989 before changing this -fun partyUpdates(block: (Party) -> Unit) { - HttpClient.newHttpClient().sendAsync( - HttpRequest.newBuilder() - .uri(URI.create("${apiUrl}/api/${apiVersion.value}/party/listen")) - .header("Accept", "text/event-stream") - .build(), - HttpResponse.BodyHandlers.ofLines() - ).thenAccept { response -> - response.body().forEach { - if (!it.startsWith("data:")) return@forEach +fun partyUpdates(block: (Party?) -> Unit) { + runConcurrent { + HttpClient.newHttpClient().sendAsync( + HttpRequest.newBuilder() + .uri(URI.create("${apiUrl}/api/${apiVersion.value}/party/listen")) + .header("Accept", "text/event-stream") + .header("Authorization", "Bearer $accessToken") + .build(), + HttpResponse.BodyHandlers.ofLines() + ).thenAccept { response -> + response.body().forEach { + if (!it.startsWith("data:")) return@forEach - val data = it.substring(5).trim() - val party = Lambda.gson.fromJson(data, Party::class.java) - block(party) - } - }.join() + val data = it.substring(5).trim() + val party = runCatching { Lambda.gson.fromJson(data, Party::class.java) }.getOrNull() + + block(party) + } + }.join() + } } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt index d0bef2f29..1563a168c 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt @@ -43,8 +43,4 @@ data class Party( // The list of players in the party. @SerializedName("players") val players: List, - - // The settings of the party - @SerializedName("settings") - val settings: Settings, ) diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt deleted file mode 100644 index 33734cb12..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Settings.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2024 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.models - -import com.google.gson.annotations.SerializedName - -data class Settings( - // The maximum number of players in the party. - // example: 10 - @SerializedName("max_players") - val maxPlayers: Int, - - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - // @SerializedName("public") - // val public: Boolean, -) From db413fa7888fdb6699d8c82fb5edd2abb4a845fd Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:13:46 -0500 Subject: [PATCH 12/30] Parked the party feature locally --- .../lambda/module/modules/client/Discord.kt | 170 +++--------------- .../network/api/v1/endpoints/CreateParty.kt | 32 ---- .../network/api/v1/endpoints/DeleteParty.kt | 33 ---- .../network/api/v1/endpoints/GetParty.kt | 32 ---- .../network/api/v1/endpoints/JoinParty.kt | 40 ----- .../network/api/v1/endpoints/LeaveParty.kt | 32 ---- .../network/api/v1/endpoints/PartyUpdates.kt | 52 ------ .../com/lambda/network/api/v1/models/Party.kt | 46 ----- .../kotlin/com/lambda/util/extension/World.kt | 1 + 9 files changed, 22 insertions(+), 416 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt delete mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 1568c5ccc..99edabf03 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -20,140 +20,57 @@ package com.lambda.module.modules.client import com.lambda.Lambda import com.lambda.context.SafeContext import com.lambda.event.EventFlow -import com.lambda.event.events.RenderEvent import com.lambda.event.events.WorldEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.event.listener.SafeListener.Companion.listenConcurrently -import com.lambda.network.api.v1.models.Party +import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.module.Module -import com.lambda.module.modules.client.Network.isDiscordLinked import com.lambda.module.modules.client.Network.updateToken import com.lambda.module.tag.ModuleTag -import com.lambda.network.api.v1.endpoints.createParty -import com.lambda.network.api.v1.endpoints.deleteParty -import com.lambda.network.api.v1.endpoints.joinParty -import com.lambda.network.api.v1.endpoints.leaveParty import com.lambda.network.api.v1.endpoints.linkDiscord -import com.lambda.network.api.v1.endpoints.partyUpdates import com.lambda.threading.runConcurrent -import com.lambda.util.Communication.toast import com.lambda.util.Communication.warn import com.lambda.util.Nameable import com.lambda.util.extension.dimensionName +import com.lambda.util.extension.worldName import dev.cbyrne.kdiscordipc.KDiscordIPC -import dev.cbyrne.kdiscordipc.core.event.DiscordEvent -import dev.cbyrne.kdiscordipc.core.event.impl.ReadyEvent import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket import dev.cbyrne.kdiscordipc.data.activity.* import kotlinx.coroutines.delay -import net.minecraft.entity.player.PlayerEntity object Discord : Module( name = "Discord", description = "Discord Rich Presence configuration", defaultTags = setOf(ModuleTag.CLIENT), -// enabledByDefault = true, // ToDo: Bring this back on beta release + //enabledByDefault = true, // ToDo: Bring this back on beta release ) { - private val page by setting("Page", Page.General) - - /* General settings */ - private val delay by setting("Update Delay", 15000L, 15000L..30000L, 100L, unit = "ms") { page == Page.General } - private val showTime by setting("Show Time", true, description = "Show how long you have been playing for.") { page == Page.General } - private val line1Left by setting("Line 1 Left", LineInfo.WORLD) { page == Page.General } - private val line1Right by setting("Line 1 Right", LineInfo.USERNAME) { page == Page.General } - private val line2Left by setting("Line 2 Left", LineInfo.DIMENSION) { page == Page.General } - private val line2Right by setting("Line 2 Right", LineInfo.FPS) { page == Page.General } - - /* Party settings */ - private val createByDefault by setting("Create By Default", true, description = "Create parties on") { page == Page.Party } + private val delay by setting("Update Delay", 5000L, 5000L..30000L, 100L, unit = "ms") + private val showTime by setting("Show Time", true, description = "Show how long you have been playing for.") + private val line1Left by setting("Line 1 Left", LineInfo.WORLD) + private val line1Right by setting("Line 1 Right", LineInfo.USERNAME) + private val line2Left by setting("Line 2 Left", LineInfo.DIMENSION) + private val line2Right by setting("Line 2 Right", LineInfo.FPS) val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) private var startup = System.currentTimeMillis() var discordAuth: AuthenticatePacket.Data? = null; private set - var currentParty: Party? = null; private set - - val PlayerEntity.isPartyOwner - get() = uuid == currentParty?.leader?.uuid - - val PlayerEntity.isInParty: Boolean - get() = currentParty?.players?.any { it.uuid == uuid } ?: false init { - rpc.subscribe() - - // ToDo: Nametag for friends when ref/ui is merged - // listen() - - listenConcurrently { + listenOnce { // If the player is in a party and this most likely means that the `onEnable` // block ran and is already handling the activity - if (rpc.connected && player.isInParty) return@listenConcurrently - - start() - handleLoop() - stop() - } + if (rpc.connected) return@listenOnce false - onDisable { stop() } - onEnable { runConcurrent { start(); handleLoop() } } - } + runConcurrent { + start() + handleLoop() + } - /** - * Creates a new party, leaves or delete the current party if there is one - */ - fun SafeContext.partyCreate() { - if (!isDiscordLinked) return warn("You did not link your discord account") - if (player.isInParty) { - if (player.isPartyOwner) deleteParty() else leaveParty() + return@listenOnce true } - val (party, error) = createParty() - if (error != null) return warn("Failed to create a party: ${error.exception}") - - currentParty = party - partyUpdates { currentParty = it } - } - - /** - * Joins a new party with the invitation ID - */ - fun SafeContext.partyJoin(id: String) { - if (!isDiscordLinked) return warn("You did not link your discord account") - - val (party, error) = joinParty(id) - if (error != null) return warn("Failed to join the party: ${error.errorData}") - - currentParty = party - partyUpdates { currentParty = it } - } - - /** - * Leaves the current party - */ - fun SafeContext.partyLeave() { - if (!isDiscordLinked) return warn("You did not link your discord account") - if (!player.isInParty) return warn("You are not in a party") - - val (_, error) = leaveParty() - if (error != null) return warn("Failed to leave the party: ${error.errorData}") - - currentParty = null - } - - /** - * Deletes the current party - */ - fun SafeContext.partyDelete() { - if (!isDiscordLinked) return warn("You did not link your discord account") - if (!player.isInParty) return warn("You are not in a party") - - val (_, error) = deleteParty() - if (error != null) return warn("Failed to delete the party: ${error.exception}") - - currentParty = null - + onEnable { runConcurrent { start(); handleLoop() } } + onDisable { stop() } } private suspend fun start() { @@ -164,10 +81,7 @@ object Discord : Module( val auth = rpc.applicationManager.authenticate() val (authResp, error) = linkDiscord(discordToken = auth.accessToken) - if (error != null) { - warn(error.message.toString()) - return toast("Failed to link the discord account to the minecraft auth") - } + if (error != null) return warn("Failed to link the discord account to the minecraft auth") updateToken(authResp) discordAuth = auth @@ -177,71 +91,29 @@ object Discord : Module( if (rpc.connected) rpc.disconnect() } - private fun KDiscordIPC.subscribe() { - on { - subscribe(DiscordEvent.VoiceChannelSelect) - subscribe(DiscordEvent.VoiceStateCreate) - subscribe(DiscordEvent.VoiceStateUpdate) - subscribe(DiscordEvent.VoiceStateDelete) - subscribe(DiscordEvent.VoiceSettingsUpdate) - subscribe(DiscordEvent.VoiceConnectionStatus) - subscribe(DiscordEvent.SpeakingStart) - subscribe(DiscordEvent.SpeakingStop) - subscribe(DiscordEvent.ActivityJoin) - subscribe(DiscordEvent.ActivityJoinRequest) - subscribe(DiscordEvent.OverlayUpdate) - // subscribe(DiscordEvent.ActivitySpectate) // Unsupported - } - } - private suspend fun SafeContext.handleLoop() { - if (isDiscordLinked) { - if (createByDefault) createParty() - partyUpdates { currentParty = it } - } - while (rpc.connected) { update() delay(delay) } - - if (isDiscordLinked) leaveParty() } private suspend fun SafeContext.update() { - val party = currentParty - rpc.activityManager.setActivity { details = "${line1Left.value(this@update)} | ${line1Right.value(this@update)}".take(128) state = "${line2Left.value(this@update)} | ${line2Right.value(this@update)}".take(128) largeImage("lambda", Lambda.VERSION) smallImage("https://mc-heads.net/avatar/${mc.gameProfile.id}/nohelm", mc.gameProfile.name) - - if (party != null) { - party(party.id.toString(), party.players.size, 20) // Placeholder while - secrets(party.joinSecret) - } else { - button("Download", "https://github.com/lambda-client/lambda") - } + button("Download", "https://github.com/lambda-client/lambda") if (showTime) timestamps(startup) } } - private enum class Page { - General, Party - } - private enum class LineInfo(val value: SafeContext.() -> String) : Nameable { VERSION({ Lambda.VERSION }), - WORLD({ - when { - mc.currentServerEntry != null -> "Multiplayer" - mc.isIntegratedServerRunning -> "Singleplayer" - else -> "Main Menu" - } - }), + WORLD({ worldName }), USERNAME({ mc.session.username }), HEALTH({ "${mc.player?.health ?: 0} HP" }), HUNGER({ "${mc.player?.hungerManager?.foodLevel ?: 0} Hunger" }), diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt deleted file mode 100644 index 415a1e10f..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/CreateParty.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 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.extensions.authentication -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party - -fun createParty() = - Fuel.post("${apiUrl}/api/${apiVersion.value}/party/create") - .authentication() - .bearer(Network.accessToken) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt deleted file mode 100644 index 468dcbff1..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/DeleteParty.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2024 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.extensions.authentication -import com.github.kittinunf.fuel.core.extensions.jsonBody -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party - -fun deleteParty() = - Fuel.delete("${apiUrl}/api/${apiVersion.value}/party/delete") - .authentication() - .bearer(Network.accessToken) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt deleted file mode 100644 index ba845950d..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetParty.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 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.extensions.authentication -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party - -fun getParty() = - Fuel.get("${apiUrl}/api/${apiVersion.value}/party") - .authentication() - .bearer(Network.accessToken) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt deleted file mode 100644 index 12e56e757..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/JoinParty.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 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.Headers -import com.github.kittinunf.fuel.core.extensions.authentication -import com.github.kittinunf.fuel.core.extensions.jsonBody -import com.github.kittinunf.fuel.gson.jsonBody -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party - -fun joinParty( - // The ID of the party. - // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - partyId: String, -) = - Fuel.put("${apiUrl}/api/${apiVersion.value}/party/join", listOf("id" to partyId)) - .jsonBody("""{ "id": "$partyId" }""") - .authentication() - .bearer(Network.accessToken) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt deleted file mode 100644 index b265cac96..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LeaveParty.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 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.extensions.authentication -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party - -fun leaveParty() = - Fuel.put("${apiUrl}/api/${apiVersion.value}/party/leave") - .authentication() - .bearer(Network.accessToken) - .responseObject().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt deleted file mode 100644 index 53454a5d3..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/PartyUpdates.kt +++ /dev/null @@ -1,52 +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.lambda.Lambda -import com.lambda.module.modules.client.Network.accessToken -import com.lambda.module.modules.client.Network.apiUrl -import com.lambda.module.modules.client.Network.apiVersion -import com.lambda.network.api.v1.models.Party -import com.lambda.threading.runConcurrent -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse - -// Waiting for https://github.com/kittinunf/fuel/issues/989 before changing this -fun partyUpdates(block: (Party?) -> Unit) { - runConcurrent { - HttpClient.newHttpClient().sendAsync( - HttpRequest.newBuilder() - .uri(URI.create("${apiUrl}/api/${apiVersion.value}/party/listen")) - .header("Accept", "text/event-stream") - .header("Authorization", "Bearer $accessToken") - .build(), - HttpResponse.BodyHandlers.ofLines() - ).thenAccept { response -> - response.body().forEach { - if (!it.startsWith("data:")) return@forEach - - val data = it.substring(5).trim() - val party = runCatching { Lambda.gson.fromJson(data, Party::class.java) }.getOrNull() - - block(party) - } - }.join() - } -} diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt deleted file mode 100644 index 1563a168c..000000000 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Party.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2024 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.models - -import com.google.gson.annotations.SerializedName -import java.util.* - -data class Party( - // The ID of the party. - // It is a random string of 30 characters. - @SerializedName("id") - val id: UUID, - - // The join secret of the party. - // It is a random string of 100 characters. - @SerializedName("join_secret") - val joinSecret: String, - - // The leader of the party - @SerializedName("leader") - val leader: Player, - - // The creation date of the party. - // example: 2021-10-10T12:00:00Z - @SerializedName("creation") - val creation: String, - - // The list of players in the party. - @SerializedName("players") - val players: List, -) diff --git a/common/src/main/kotlin/com/lambda/util/extension/World.kt b/common/src/main/kotlin/com/lambda/util/extension/World.kt index 34ab2dc54..7c3713ed4 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/World.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/World.kt @@ -27,6 +27,7 @@ import net.minecraft.util.math.BlockPos import net.minecraft.world.World import java.awt.Color +val SafeContext.worldName: String get() = when { mc.currentServerEntry != null -> "Multiplayer"; mc.isIntegratedServerRunning -> "Singleplayer"; else -> "Main Menu" } val SafeContext.isOverworld: Boolean get() = world.registryKey == World.OVERWORLD val SafeContext.isNether: Boolean get() = world.registryKey == World.NETHER val SafeContext.isEnd: Boolean get() = world.registryKey == World.END From b925daac72af4d845d322b053e5d2c7c95ec429b Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:18:42 -0500 Subject: [PATCH 13/30] Removed useless code --- .../lambda/module/modules/client/Network.kt | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) 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 cafe2c744..7bf2cb5c6 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 @@ -17,7 +17,6 @@ package com.lambda.module.modules.client -import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.Lambda.gson import com.lambda.Lambda.mc @@ -34,7 +33,6 @@ import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.network.api.v1.models.Authentication.Data -import com.lambda.util.Communication.info import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler @@ -53,14 +51,14 @@ object Network : Module( defaultTags = setOf(ModuleTag.CLIENT), enabledByDefault = true, ) { - 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 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) - var apiAuth: Authentication? = null; private set // TODO: Cache - var deserialized: Data? = null; private set // gson is too stupid + private var auth: Authentication? = null // TODO: Cache + private var deserialized: Data? = null val accessToken: String - get() = apiAuth?.accessToken ?: "" + get() = auth?.accessToken ?: "" val SafeContext.isDiscordLinked: Boolean get() = deserialized?.data?.discordId != null @@ -80,53 +78,42 @@ object Network : Module( } listenOnceUnsafe { - if (!::hash.isInitialized) { - if (!authenticate()) return@listenOnceUnsafe false - } + if (mc.gameProfile.isOffline) return@listenOnceUnsafe true // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets // and posted to the sessionserver api - val (authResponse, error) = login(mc.session.username, hash) + val (resp, error) = login(mc.session.username, hash) if (error != null) { LOG.debug("Unable to authenticate: ${error.message}") return@listenOnceUnsafe false } - apiAuth = authResponse + auth = resp deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) - LOG.info("Successfully authenticated") true } listenUnsafeConcurrently { authenticate() } } - private fun authenticate(): Boolean { - if (mc.gameProfile.isOffline) return true - - val addddd = ServerAddress.parse(authServer) + private fun authenticate() { + val address = ServerAddress.parse(authServer) val connection = ClientConnection(CLIENTBOUND) - val addr = AllowedAddressResolver.DEFAULT.resolve(addddd) + val resolved = AllowedAddressResolver.DEFAULT.resolve(address) .map { it.inetSocketAddress }.get() - runCatching { - ClientConnection.connect(addr, mc.options.shouldUseNativeTransport(), connection) - .syncUninterruptibly() - }.onFailure { - return false - } + ClientConnection.connect(resolved, mc.options.shouldUseNativeTransport(), connection) + .syncUninterruptibly() val handler = ClientLoginNetworkHandler(connection, mc, null, null, false, null) { Text.empty() } - connection.connect(addr.hostName, addr.port, handler) + connection.connect(resolved.hostName, resolved.port, handler) connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) - - return true } - internal fun updateToken(auth: Authentication?) { apiAuth = auth } + internal fun updateToken(auth: Authentication?) { this.auth = auth } enum class ApiVersion(val value: String) { // We can use @Deprecated("Not supported") to remove old API versions in the future From 87a9700cf6aa27ce4454b3d9238f3966e04bbc85 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:18:52 -0500 Subject: [PATCH 14/30] Delete DiscordCommand.kt --- .../lambda/command/commands/DiscordCommand.kt | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt diff --git a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt deleted file mode 100644 index c5f893358..000000000 --- a/common/src/main/kotlin/com/lambda/command/commands/DiscordCommand.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 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.command.commands - -import com.lambda.brigadier.argument.literal -import com.lambda.brigadier.argument.value -import com.lambda.brigadier.argument.word -import com.lambda.brigadier.execute -import com.lambda.brigadier.required -import com.lambda.command.LambdaCommand -import com.lambda.module.modules.client.Discord.partyCreate -import com.lambda.module.modules.client.Discord.partyDelete -import com.lambda.module.modules.client.Discord.partyJoin -import com.lambda.module.modules.client.Discord.partyLeave -import com.lambda.module.modules.client.Discord.rpc -import com.lambda.network.api.v1.endpoints.deleteParty -import com.lambda.network.api.v1.endpoints.leaveParty -import com.lambda.threading.runConcurrent -import com.lambda.threading.runSafe -import com.lambda.util.extension.CommandBuilder - -object DiscordCommand : LambdaCommand( - name = "discord", - description = "Discord Rich Presence commands", - usage = "rpc " -) { - override fun CommandBuilder.create() { - required(literal("create")) { - execute { - runSafe { partyCreate() } - } - } - - required(literal("leave")) { - execute { - runSafe { partyLeave() } - } - } - - required(literal("delete")) { - execute { - runSafe { partyDelete() } - } - } - - required(literal("join")) { - required(word("id")) { id -> - execute { - runSafe { partyJoin(id().value()) } - } - } - } - - required(literal("accept")) { - required(word("user")) { user -> - execute { - runConcurrent { rpc.activityManager.acceptJoinRequest(user().value()) } - } - } - } - - required(literal("refuse")) { - required(word("user")) { user -> - execute { - runConcurrent { rpc.activityManager.refuseJoinRequest(user().value()) } - } - } - } - } -} From dae565972034744ba2370f9b03094d8e1d7e0f86 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:19:51 -0500 Subject: [PATCH 15/30] Added module description --- .../main/kotlin/com/lambda/module/modules/client/Network.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7bf2cb5c6..9723ef110 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 @@ -47,7 +47,7 @@ import java.util.* object Network : Module( name = "Network", - description = "...", + description = "Lambda Authentication", defaultTags = setOf(ModuleTag.CLIENT), enabledByDefault = true, ) { @@ -55,7 +55,7 @@ object Network : Module( val apiUrl by setting("API Server", "https://api.lambda-client.org") val apiVersion by setting("API Version", ApiVersion.V1) - private var auth: Authentication? = null // TODO: Cache + private var auth: Authentication? = null private var deserialized: Data? = null val accessToken: String get() = auth?.accessToken ?: "" From 106d137a2a9399bfb3b514d856ab5ee660272e7d Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sun, 23 Feb 2025 16:19:57 -0500 Subject: [PATCH 16/30] Removed todo --- .../src/main/kotlin/com/lambda/module/modules/client/Discord.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 99edabf03..79b9c504c 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -76,7 +76,7 @@ object Discord : Module( private suspend fun start() { if (rpc.connected) return - runConcurrent { rpc.connect() } // TODO: Create a function that will wait until x seconds has passed or if the connection is successful + runConcurrent { rpc.connect() } delay(1000) val auth = rpc.applicationManager.authenticate() From 9eb26ad55e3ab98b73ab5e62c3a11f4c4e85078f Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 28 Feb 2025 13:53:30 -0500 Subject: [PATCH 17/30] Push capes --- .../render/CapeFeatureRendererMixin.java | 32 ++++++++++++++++ .../lambda/module/modules/client/Network.kt | 15 +++++--- .../network/api/v1/endpoints/GetCape.kt | 38 +++++++++++++++++++ .../network/api/v1/endpoints/SetCape.kt | 36 ++++++++++++++++++ .../main/resources/lambda.mixins.common.json | 1 + 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt diff --git a/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java new file mode 100644 index 000000000..b0eebf2e7 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java @@ -0,0 +1,32 @@ +/* + * 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.render; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.render.entity.feature.CapeFeatureRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(CapeFeatureRenderer.class) +public class CapeFeatureRendererMixin { + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/network/AbstractClientPlayerEntity;FFFFFF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;capeTexture()Lnet/minecraft/util/Identifier;")) + Identifier renderCape(Identifier original) { + return new Identifier("lambda", "primary.png"); + } +} 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 9723ef110..4a93e8018 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 @@ -28,10 +28,10 @@ import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionResponse import com.lambda.event.listener.UnsafeListener.Companion.listenOnceUnsafe import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently -import com.lambda.network.api.v1.endpoints.login -import com.lambda.network.api.v1.models.Authentication import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.network.api.v1.endpoints.login +import com.lambda.network.api.v1.models.Authentication import com.lambda.network.api.v1.models.Authentication.Data import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver @@ -45,6 +45,7 @@ import net.minecraft.text.Text import java.math.BigInteger import java.util.* + object Network : Module( name = "Network", description = "Lambda Authentication", @@ -78,7 +79,7 @@ object Network : Module( } listenOnceUnsafe { - if (mc.gameProfile.isOffline) return@listenOnceUnsafe true + if (mc.gameProfile.isOffline) return@listenOnceUnsafe true // ToDo: If the player have the properties but are invalid this doesn't work // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets @@ -89,8 +90,7 @@ object Network : Module( return@listenOnceUnsafe false } - auth = resp - deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) + updateToken(resp) true } @@ -113,7 +113,10 @@ object Network : Module( connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) } - internal fun updateToken(auth: Authentication?) { this.auth = auth } + internal fun updateToken(resp: Authentication?) { + auth = resp + deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) + } enum class ApiVersion(val value: String) { // We can use @Deprecated("Not supported") to remove old API versions in the future 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 new file mode 100644 index 000000000..897b2bbfb --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt @@ -0,0 +1,38 @@ +/* + * 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.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion +import java.util.UUID + +/** + * Get the cape of the given player UUID. + * + * input: + * - ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 + * + * output: + * - cape_1 + */ +fun getCape( + uuid: UUID, +) = + Fuel.put("$apiUrl/api/${apiVersion.value}/cape", listOf("id" to uuid.toString())) + .responseString() 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 new file mode 100644 index 000000000..075beed4f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt @@ -0,0 +1,36 @@ +/* + * 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.extensions.authentication +import com.lambda.module.modules.client.Network +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion +import java.util.* + +fun setCape( + // Get the cape of the given player UUID. + // example: ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 + // example: ab24f5d6dcf145e4897eb50a7c5e7422 + uuid: UUID, +) = + Fuel.put("$apiUrl/api/${apiVersion.value}/cape", listOf("id" to uuid.toString())) + .authentication() + .bearer(Network.accessToken) + .responseString() diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index dbc6369e0..b9e98d59d 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -28,6 +28,7 @@ "render.BackgroundRendererMixin", "render.BlockRenderManagerMixin", "render.CameraMixin", + "render.CapeFeatureRendererMixin", "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", From e070b3ceb91c2e41b6d49d394689eff756f997cf Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 6 Mar 2025 19:52:52 -0500 Subject: [PATCH 18/30] Update API to v1.1.0 --- .../ClientPlayNetworkHandlerMixin.java | 15 ++++ .../render/CapeFeatureRendererMixin.java | 11 ++- .../render/ElytraFeatureRendererMixin.java | 39 +++++++++ common/src/main/kotlin/com/lambda/Lambda.kt | 39 --------- .../lambda/command/commands/CapeCommand.kt | 55 ++++++++++++ .../config/configurations/UserConfig.kt | 27 ++++++ .../com/lambda/event/events/WorldEvent.kt | 24 ++++++ .../com/lambda/module/modules/client/Capes.kt | 27 ++++++ .../lambda/module/modules/client/Discord.kt | 8 +- .../lambda/module/modules/client/Network.kt | 48 +++-------- .../kotlin/com/lambda/network/CapeManager.kt | 72 ++++++++++++++++ .../com/lambda/network/NetworkManager.kt | 85 +++++++++++++++++++ .../network/api/v1/endpoints/GetCape.kt | 20 ++--- .../network/api/v1/endpoints/LinkDiscord.kt | 18 ++-- .../lambda/network/api/v1/endpoints/Login.kt | 19 +++-- .../network/api/v1/endpoints/SetCape.kt | 25 +++--- .../com/lambda/network/api/v1/models/Cape.kt | 54 ++++++++++++ .../kotlin/com/lambda/util/FolderRegister.kt | 1 + .../kotlin/com/lambda/util/StringUtils.kt | 33 +++++++ .../kotlin/com/lambda/util/extension/Other.kt | 5 ++ .../lambda/util/reflections/Reflections.kt | 6 +- .../main/resources/lambda.mixins.common.json | 7 +- 22 files changed, 516 insertions(+), 122 deletions(-) create mode 100644 common/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java create mode 100644 common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt create mode 100644 common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt create mode 100644 common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt create mode 100644 common/src/main/kotlin/com/lambda/network/CapeManager.kt create mode 100644 common/src/main/kotlin/com/lambda/network/NetworkManager.kt create mode 100644 common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt diff --git a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java index 7fd8cfbdc..6c7d2263d 100644 --- a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java +++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java @@ -21,7 +21,9 @@ import com.lambda.event.events.InventoryEvent; import com.lambda.event.events.WorldEvent; import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.network.PlayerListEntry; import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket; import org.spongepowered.asm.mixin.Mixin; @@ -36,6 +38,19 @@ void injectJoinPacket(GameJoinS2CPacket packet, CallbackInfo ci) { EventFlow.post(new WorldEvent.Join()); } + @Inject(method = "handlePlayerListAction(Lnet/minecraft/network/packet/s2c/play/PlayerListS2CPacket$Action;Lnet/minecraft/network/packet/s2c/play/PlayerListS2CPacket$Entry;Lnet/minecraft/client/network/PlayerListEntry;)V", at = @At("TAIL")) + void injectPlayerList(PlayerListS2CPacket.Action action, PlayerListS2CPacket.Entry receivedEntry, PlayerListEntry currentEntry, CallbackInfo ci) { + if (action != PlayerListS2CPacket.Action.ADD_PLAYER) return; + + var name = currentEntry.getProfile().getName(); + var uuid = currentEntry.getProfile().getId(); + + if (receivedEntry.listed()) + EventFlow.post(new WorldEvent.Player.Join(name, uuid, currentEntry)); + else + EventFlow.post(new WorldEvent.Player.Leave(name, uuid, currentEntry)); + } + @Inject(method = "onUpdateSelectedSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V", shift = At.Shift.AFTER), cancellable = true) private void onUpdateSelectedSlot(UpdateSelectedSlotS2CPacket packet, CallbackInfo ci) { if (EventFlow.post(new InventoryEvent.HotbarSlot.Sync(packet.getSlot())).isCanceled()) ci.cancel(); diff --git a/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java index b0eebf2e7..309978183 100644 --- a/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.java @@ -17,8 +17,13 @@ package com.lambda.mixin.render; +import com.lambda.module.modules.client.Capes; +import com.lambda.network.CapeManager; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.entity.feature.CapeFeatureRenderer; +import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -26,7 +31,9 @@ @Mixin(CapeFeatureRenderer.class) public class CapeFeatureRendererMixin { @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/network/AbstractClientPlayerEntity;FFFFFF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;capeTexture()Lnet/minecraft/util/Identifier;")) - Identifier renderCape(Identifier original) { - return new Identifier("lambda", "primary.png"); + Identifier renderCape(Identifier original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity player, float f, float g, float h, float j, float k, float l) { + if (!Capes.INSTANCE.isEnabled() || !CapeManager.INSTANCE.containsKey(player.getUuid())) return original; + + return Identifier.of("lambda", CapeManager.INSTANCE.get(player.getUuid())); } } diff --git a/common/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java new file mode 100644 index 000000000..4d7f5b897 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/ElytraFeatureRendererMixin.java @@ -0,0 +1,39 @@ +/* + * 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.render; + +import com.lambda.module.modules.client.Capes; +import com.lambda.network.CapeManager; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.ElytraFeatureRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ElytraFeatureRenderer.class) +public class ElytraFeatureRendererMixin { + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/entity/LivingEntity;FFFFFF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;elytraTexture()Lnet/minecraft/util/Identifier;")) + Identifier renderElytra(Identifier original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, T livingEntity, float f, float g, float h, float j, float k, float l) { + if (!Capes.INSTANCE.isEnabled() || !CapeManager.INSTANCE.containsKey(livingEntity.getUuid())) return original; + + return Identifier.of("lambda", CapeManager.INSTANCE.get(livingEntity.getUuid())); + } +} diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index 084aeb8eb..a88c0b570 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -27,7 +27,6 @@ import com.lambda.core.Loader import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow import com.lambda.gui.impl.clickgui.windows.tag.TagWindow import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runConcurrent import com.lambda.util.KeyCode import com.mojang.authlib.GameProfile import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall @@ -39,12 +38,7 @@ import net.minecraft.util.math.BlockPos import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import java.awt.Color -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse import java.util.* -import java.util.concurrent.CountDownLatch object Lambda { @@ -76,39 +70,6 @@ object Lambda { .create() fun initialize(block: (Long) -> Unit) { - runConcurrent { - // Create an HttpClient - val client = HttpClient.newHttpClient() - - // Define the URI for the SSE stream - val uri = URI.create("http://localhost:8080/api/v1/party/listen") - - // Create an HttpRequest for the SSE stream - val request = HttpRequest.newBuilder() - .uri(uri) - .header("Accept", "text/event-stream") - .build() - - // Create a CountDownLatch to wait for the events - val latch = CountDownLatch(1) - - // Send the request and handle the response asynchronously - client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) - .thenAccept { response -> - println("Connected to SSE stream") - response.body().forEach { line -> - if (line.startsWith("data:")) { - val data = line.substring(5).trim() - println("Received event data: $data") - } - } - latch.countDown() - } - - // Wait until the response is received and handled - latch.await() - } - recordRenderCall { block(Loader.initialize()) } diff --git a/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt new file mode 100644 index 000000000..0427ce56d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt @@ -0,0 +1,55 @@ +/* + * 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.command.commands + +import com.lambda.brigadier.CommandResult.Companion.failure +import com.lambda.brigadier.CommandResult.Companion.success +import com.lambda.brigadier.argument.string +import com.lambda.brigadier.argument.value +import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.required +import com.lambda.command.LambdaCommand +import com.lambda.network.NetworkManager +import com.lambda.network.api.v1.endpoints.setCape +import com.lambda.util.extension.CommandBuilder + +object CapeCommand : LambdaCommand( + name = "cape", + usage = "set ", + description = "Sets your cape", +) { + override fun CommandBuilder.create() { + required(string("id")) { id -> + suggests { _, builder -> + NetworkManager.capes + .forEach { builder.suggest(it) } + + builder.buildFuture() + } + + executeWithResult { + val cape = id().value() + setCape(cape) + .fold( + success = { NetworkManager.cape = cape; success() }, + failure = { failure(it) }, + ) + } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt new file mode 100644 index 000000000..c3279e24b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt @@ -0,0 +1,27 @@ +/* + * 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.config.configurations + +import com.lambda.config.Configuration +import com.lambda.util.FolderRegister +import java.io.File + +object UserConfig : Configuration() { + override val configName get() = "preferences" + override val primary: File = FolderRegister.config.resolve("$configName.json").toFile() +} diff --git a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt index 6e9f60d7b..2dea21a0b 100644 --- a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt @@ -24,9 +24,12 @@ import com.lambda.threading.runSafe import com.lambda.util.BlockUtils.blockState import net.minecraft.block.BlockState import net.minecraft.block.Blocks +import net.minecraft.client.network.PlayerListEntry import net.minecraft.util.math.BlockPos import net.minecraft.util.shape.VoxelShape import net.minecraft.world.chunk.WorldChunk +import java.util.UUID +import kotlin.uuid.Uuid /** * Represents various events that can occur within the world. @@ -40,6 +43,27 @@ sealed class WorldEvent { // Represents the player joining the world class Join() : Event + // ToDo: Maybe create a network event seal with some s2c events + sealed class Player { + /** + * Event triggered upon player joining + */ + data class Join( + val name: String, + val uuid: UUID, + val entry: PlayerListEntry, + ) : Event + + /** + * Event triggered upon player leaving + */ + data class Leave( + val name: String, + val uuid: UUID, + val entry: PlayerListEntry, + ) : Event + } + /** * Represents an event specific to chunk operations within the world. * diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt new file mode 100644 index 000000000..e56d19edc --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt @@ -0,0 +1,27 @@ +/* + * 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.module.modules.client + +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag + +object Capes : Module( + name = "Capes", + description = "Display custom capes", + defaultTags = setOf(ModuleTag.CLIENT), +) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 79b9c504c..b025dafd8 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -23,8 +23,8 @@ import com.lambda.event.EventFlow import com.lambda.event.events.WorldEvent import com.lambda.event.listener.SafeListener.Companion.listenOnce import com.lambda.module.Module -import com.lambda.module.modules.client.Network.updateToken import com.lambda.module.tag.ModuleTag +import com.lambda.network.NetworkManager.updateToken import com.lambda.network.api.v1.endpoints.linkDiscord import com.lambda.threading.runConcurrent import com.lambda.util.Communication.warn @@ -83,7 +83,7 @@ object Discord : Module( val (authResp, error) = linkDiscord(discordToken = auth.accessToken) if (error != null) return warn("Failed to link the discord account to the minecraft auth") - updateToken(authResp) + updateToken(authResp!!) discordAuth = auth } @@ -115,8 +115,8 @@ object Discord : Module( VERSION({ Lambda.VERSION }), WORLD({ worldName }), USERNAME({ mc.session.username }), - HEALTH({ "${mc.player?.health ?: 0} HP" }), - HUNGER({ "${mc.player?.hungerManager?.foodLevel ?: 0} Hunger" }), + HEALTH({ "${player.health} HP" }), + HUNGER({ "${player.hungerManager.foodLevel} Hunger" }), DIMENSION({ dimensionName }), FPS({ "${mc.currentFps} FPS" }); } 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 4a93e8018..2b62e7b9b 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 @@ -18,32 +18,27 @@ package com.lambda.module.modules.client import com.lambda.Lambda.LOG -import com.lambda.Lambda.gson import com.lambda.Lambda.mc -import com.lambda.context.SafeContext import com.lambda.event.events.ClientEvent import com.lambda.event.events.ConnectionEvent -import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionRequest import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionResponse -import com.lambda.event.listener.UnsafeListener.Companion.listenOnceUnsafe import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafeConcurrently import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.network.NetworkManager +import com.lambda.network.NetworkManager.updateToken import com.lambda.network.api.v1.endpoints.login -import com.lambda.network.api.v1.models.Authentication -import com.lambda.network.api.v1.models.Authentication.Data +import com.lambda.util.StringUtils.hash import com.lambda.util.extension.isOffline import net.minecraft.client.network.AllowedAddressResolver import net.minecraft.client.network.ClientLoginNetworkHandler import net.minecraft.client.network.ServerAddress import net.minecraft.network.ClientConnection import net.minecraft.network.NetworkSide.CLIENTBOUND -import net.minecraft.network.encryption.NetworkEncryptionUtils import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket import net.minecraft.text.Text import java.math.BigInteger -import java.util.* object Network : Module( @@ -56,30 +51,24 @@ object Network : Module( val apiUrl by setting("API Server", "https://api.lambda-client.org") val apiVersion by setting("API Version", ApiVersion.V1) - private var auth: Authentication? = null - private var deserialized: Data? = null - val accessToken: String - get() = auth?.accessToken ?: "" - - val SafeContext.isDiscordLinked: Boolean - get() = deserialized?.data?.discordId != null - - private lateinit var serverId: String private lateinit var hash: String init { - listenUnsafe { serverId = it.serverId } + listenUnsafeConcurrently { authenticate() } listenUnsafe { event -> if (event.secretKey.isDestroyed) return@listenUnsafe - hash = BigInteger( - NetworkEncryptionUtils.computeServerId(serverId, event.publicKey, event.secretKey) - ).toString(16) + // Server id is always empty when sent by the Notchian server + val computed = byteArrayOf() + .hash("SHA-1", event.secretKey.encoded, event.publicKey.encoded) + + hash = BigInteger(computed).toString(16) } - listenOnceUnsafe { - if (mc.gameProfile.isOffline) return@listenOnceUnsafe true // ToDo: If the player have the properties but are invalid this doesn't work + listenUnsafe { + // FixMe: If the player have the properties but are invalid this doesn't work + if (NetworkManager.isValid || mc.gameProfile.isOffline) return@listenUnsafe // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets @@ -87,15 +76,11 @@ object Network : Module( val (resp, error) = login(mc.session.username, hash) if (error != null) { LOG.debug("Unable to authenticate: ${error.message}") - return@listenOnceUnsafe false + return@listenUnsafe } - updateToken(resp) - - true + updateToken(resp!!) } - - listenUnsafeConcurrently { authenticate() } } private fun authenticate() { @@ -113,11 +98,6 @@ object Network : Module( connection.send(LoginHelloC2SPacket(mc.session.username, mc.session.uuidOrNull)) } - internal fun updateToken(resp: Authentication?) { - auth = resp - deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(accessToken.split(".")[1])), Data::class.java) - } - enum class ApiVersion(val value: String) { // We can use @Deprecated("Not supported") to remove old API versions in the future V1("v1"), diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt new file mode 100644 index 000000000..fc24d9ecd --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -0,0 +1,72 @@ +/* + * 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 + +import com.github.kittinunf.fuel.core.FuelError +import com.lambda.Lambda.LOG +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.core.Loadable +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.network.api.v1.endpoints.getCape +import com.lambda.sound.SoundManager.toIdentifier +import com.lambda.util.FolderRegister.capes +import net.minecraft.client.texture.NativeImage.read +import net.minecraft.client.texture.NativeImageBackedTexture +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.extension +import kotlin.io.path.inputStream +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.walk + +@OptIn(ExperimentalPathApi::class) +@Suppress("JavaIoSerializableObjectMustHaveReadResolve") +object CapeManager : ConcurrentHashMap(), Loadable { + /** + * We want to cache images to reduce cloudflare requests and save money + */ + private val images = capes.walk() + .filter { it.extension == "png" } + .associate { it.nameWithoutExtension to NativeImageBackedTexture(read(it.inputStream())) } + .onEach { (key, value) -> mc.textureManager.registerTexture(key.toIdentifier(), value) } + + /** + * Fetches the cape of the given player id + * + * @throws FuelError if something wrong happens + */ + fun SafeContext.fetch(uuid: UUID) = getOrPut(uuid) { + getCape(uuid) + .fold( + success = { if (!images.contains(it.cape)) it.fetch(); put(uuid, it.cape) }, + failure = { throw it }, + ) + } + + override fun load() = "Loaded ${images.size} cached capes" + + init { + listen(alwaysListen = true) { + runCatching { fetch(it.uuid) } + .onFailure { LOG.error(it) } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/network/NetworkManager.kt b/common/src/main/kotlin/com/lambda/network/NetworkManager.kt new file mode 100644 index 000000000..1f483f354 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/NetworkManager.kt @@ -0,0 +1,85 @@ +/* + * 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 + +import com.lambda.Lambda.gson +import com.lambda.Lambda.mc +import com.lambda.config.Configurable +import com.lambda.config.configurations.UserConfig +import com.lambda.core.Loadable +import com.lambda.network.api.v1.models.Authentication +import com.lambda.network.api.v1.models.Authentication.Data +import com.lambda.util.reflections.getResources +import java.io.File +import java.util.* + +object NetworkManager : Configurable(UserConfig), Loadable { + override val name = "network" + + var accessToken by setting("authentication", ""); private set + private var _cape by setting("cape", "") + + val isDiscordLinked: Boolean + get() = deserialized?.data?.discordId != null + + /** + * Returns the current cape id or null if there are none + */ + var cape: String? = null + get() = _cape.ifEmpty { null } + set(value) { value?.let { CapeManager.put(mc.gameProfile.id, it) }; field = value } + + /** + * Returns whether the auth has expired + */ + val isExpired: Boolean + get() = (deserialized?.expirationDate ?: 0) < System.currentTimeMillis() + + /** + * Returns whether the auth token is invalid or not + */ + val isValid: Boolean + get() = mc.gameProfile.name == deserialized?.data?.name && + mc.gameProfile.id == deserialized?.data?.uuid && + !isExpired + + private var deserialized: Data? = null + + // ToDo: Fetch remote file instead of checking local files + val capes = getResources(".*.png") + .filter { it.contains("capes") } // filterByInput hangs the program + .map { File(it).nameWithoutExtension } + + fun updateToken(resp: Authentication) { + accessToken = resp.accessToken + decodeAuth(accessToken) + } + + private fun decodeAuth(token: String) { + val payload = token.split(".").getOrNull(1) ?: return + deserialized = gson.fromJson(String(Base64.getUrlDecoder().decode(payload)), Data::class.java) + } + + override fun load(): String { + decodeAuth(accessToken) + + // ToDo: Re-authenticate every 24 hours + + return "Loaded ${capes.size} capes" + } +} 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 897b2bbfb..3a2c2a7ed 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 @@ -18,21 +18,21 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion +import com.lambda.network.api.v1.models.Cape import java.util.UUID /** - * Get the cape of the given player UUID. + * Gets the cape of the given player UUID * - * input: - * - ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 + * Example: + * - id: ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 * - * output: - * - cape_1 + * response: [Cape] or error */ -fun getCape( - uuid: UUID, -) = - Fuel.put("$apiUrl/api/${apiVersion.value}/cape", listOf("id" to uuid.toString())) - .responseString() +fun getCape(uuid: UUID) = + Fuel.get("$apiUrl/api/${apiVersion.value}/cape?id=$uuid") + .responseObject() + .third 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 e357a390a..0e4b36b29 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 @@ -21,18 +21,22 @@ import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.module.modules.client.Network import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion +import com.lambda.network.NetworkManager import com.lambda.network.api.v1.models.Authentication -fun linkDiscord( - // The player's Discord token. - // example: OTk1MTU1NzcyMzYxMTQ2NDM4 - discordToken: String, -) = +/** + * Links a Discord account to a session account + * + * Example: + * - token: OTk1MTU1NzcyMzYxMTQ2NDM4 + * + * response: [Authentication] or error + */ +fun linkDiscord(discordToken: String) = Fuel.post("${apiUrl}/api/${apiVersion.value}/link/discord") .jsonBody("""{ "token": "$discordToken" }""") .authentication() - .bearer(Network.accessToken) + .bearer(NetworkManager.accessToken) .responseObject().third 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 e6d411fce..c47e95f02 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 @@ -24,15 +24,16 @@ import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Authentication -fun login( - // The player's username. - // example: "Notch" - username: String, - - // The player's Mojang session hash. - // example: 069a79f444e94726a5befca90e38aaf5 - hash: String, -) = +/** + * Creates a new session account with mojang session hashes + * + * Example: + * - username: Notch + * - hash: 069a79f444e94726a5befca90e38aaf5 + * + * response: [Authentication] or error + */ +fun login(username: String, hash: String) = Fuel.post("${apiUrl}/api/${apiVersion.value}/login") .jsonBody("""{ "username": "$username", "hash": "$hash" }""") .responseObject().third 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 075beed4f..ef6457d1b 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 @@ -19,18 +19,21 @@ package com.lambda.network.api.v1.endpoints import com.github.kittinunf.fuel.Fuel import com.github.kittinunf.fuel.core.extensions.authentication -import com.lambda.module.modules.client.Network +import com.github.kittinunf.fuel.core.responseUnit import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion -import java.util.* +import com.lambda.network.NetworkManager -fun setCape( - // Get the cape of the given player UUID. - // example: ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 - // example: ab24f5d6dcf145e4897eb50a7c5e7422 - uuid: UUID, -) = - Fuel.put("$apiUrl/api/${apiVersion.value}/cape", listOf("id" to uuid.toString())) +/** + * Sets the currently authenticated player's cape + * + * Example: + * - id: galaxy + * + * response: [Unit] or error + */ +fun setCape(id: String) = + Fuel.put("$apiUrl/api/${apiVersion.value}/cape?id=$id") .authentication() - .bearer(Network.accessToken) - .responseString() + .bearer(NetworkManager.accessToken) + .responseUnit().third diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt new file mode 100644 index 000000000..ab1897d7c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt @@ -0,0 +1,54 @@ +/* + * 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.models + +import com.github.kittinunf.fuel.Fuel +import com.google.gson.annotations.SerializedName +import com.lambda.sound.SoundManager.toIdentifier +import com.lambda.threading.runSafe +import com.lambda.util.FolderRegister.capes +import com.lambda.util.extension.resolveFile +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.NativeImageBackedTexture +import java.io.ByteArrayOutputStream + +class Cape( + @SerializedName("url") + val url: String, + + @SerializedName("type") + val cape: String, +) { + fun fetch() = runSafe { + val output = ByteArrayOutputStream(2048*1024*4) + + Fuel.download(url) + .streamDestination { _, request -> output to { request.body.toStream() } } + + val image = NativeImage.read(output.toByteArray()) + val native = NativeImageBackedTexture(image) + val id = cape.toIdentifier() + + mc.textureManager.registerTexture(id, native) + + capes.resolveFile("$cape.png") + .writeBytes(output.toByteArray()) + } + + fun identifier() = cape.toIdentifier() +} diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index b436dff5d..49a1f5442 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -47,6 +47,7 @@ object FolderRegister : Loadable { val packetLogs: Path = lambda.resolve("packet-log") val replay: Path = lambda.resolve("replay") val cache: Path = lambda.resolve("cache") + val capes: Path = cache.resolve("capes") val structure: Path = lambda.resolve("structure") override fun load(): String { diff --git a/common/src/main/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt index ef1ce4fdf..0af9eb225 100644 --- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -17,6 +17,8 @@ package com.lambda.util +import java.security.MessageDigest + object StringUtils { /** * Returns a sanitized file path for both Unix and Linux systems @@ -89,4 +91,35 @@ object StringUtils { return cost[len0 - 1] } + + /** + * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification + * + * @receiver The string to hash + * @param algorithm The algorithm instance to use + * @param extra Additional data to digest with the string + * + * @return The string representation of the hash + */ + fun String.hash(algorithm: String, vararg extra: ByteArray): String = + MessageDigest + .getInstance(algorithm) + .apply { update(toByteArray()); extra.forEach(::update) } + .digest() + .joinToString(separator = "") { "%02x".format(it) } + + /** + * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification + * + * @receiver The byte array to hash + * @param algorithm The algorithm instance to use + * @param extra Additional data to digest with the bytearray + * + * @return The digested data + */ + fun ByteArray.hash(algorithm: String, vararg extra: ByteArray): ByteArray = + MessageDigest + .getInstance(algorithm) + .apply { update(this@hash); extra.forEach(::update) } + .digest() } 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 778e45c58..36afecef7 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Other.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Other.kt @@ -18,6 +18,8 @@ package com.lambda.util.extension import com.mojang.authlib.GameProfile +import java.io.File +import java.nio.file.Path import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -30,3 +32,6 @@ val Class<*>.isObject: Boolean val Class<*>.objectInstance: Any get() = declaredFields.first { it.name == "INSTANCE" }.apply { isAccessible = true }.get(null) + +fun Path.resolveFile(other: String): File = + resolve(other).toFile() diff --git a/common/src/main/kotlin/com/lambda/util/reflections/Reflections.kt b/common/src/main/kotlin/com/lambda/util/reflections/Reflections.kt index e91ce1fbd..471233210 100644 --- a/common/src/main/kotlin/com/lambda/util/reflections/Reflections.kt +++ b/common/src/main/kotlin/com/lambda/util/reflections/Reflections.kt @@ -20,9 +20,11 @@ package com.lambda.util.reflections import com.lambda.util.extension.isObject import com.lambda.util.extension.objectInstance import org.reflections.Reflections +import org.reflections.scanners.Scanners import org.reflections.util.ConfigurationBuilder import java.lang.reflect.Modifier -import java.util.Objects +import java.util.* + val cache = mutableMapOf() @@ -59,7 +61,7 @@ inline fun getInstances(block: ConfigurationBuilder.() -> Unit * * @return A set of resource paths that match the specified pattern. */ -inline fun getResources(pattern: String, block: ConfigurationBuilder.() -> Unit = { forPackage("com.lambda") }): Set { +inline fun getResources(pattern: String, block: ConfigurationBuilder.() -> Unit = { forPackage("com.lambda"); addScanners(Scanners.Resources) }): Set { val config = ConfigurationBuilder().apply(block) val cacheKey = Objects.hash(config.classLoaders, config.urls, config.scanners, config.inputsFilter) diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index b9e98d59d..8ca415bc8 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -14,6 +14,7 @@ "entity.FireworkRocketEntityMixin", "entity.LivingEntityMixin", "entity.PlayerEntityMixin", + "entity.PlayerInventoryMixin", "input.KeyBindingMixin", "input.KeyboardMixin", "input.MouseMixin", @@ -32,6 +33,7 @@ "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", + "render.ElytraFeatureRendererMixin", "render.GameRendererMixin", "render.GlStateManagerMixin", "render.InGameHudMixin", @@ -55,8 +57,5 @@ ], "injectors": { "defaultRequire": 1 - }, - "mixins": [ - "entity.PlayerInventoryMixin" - ] + } } From ccb863fa0fcdd31f13bd91edcbef475608e6fde7 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:04:20 -0500 Subject: [PATCH 19/30] Fold api responses --- .../com/lambda/module/modules/client/Discord.kt | 9 +++++---- .../com/lambda/module/modules/client/Network.kt | 12 +++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index b025dafd8..15cb71d1e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -80,11 +80,12 @@ object Discord : Module( delay(1000) val auth = rpc.applicationManager.authenticate() - val (authResp, error) = linkDiscord(discordToken = auth.accessToken) - if (error != null) return warn("Failed to link the discord account to the minecraft auth") - updateToken(authResp!!) - discordAuth = auth + linkDiscord(discordToken = auth.accessToken) + .fold( + success = { updateToken(it); discordAuth = auth }, + failure = { warn("Failed to link the discord account to the minecraft auth") }, + ) } private fun stop() { 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 2b62e7b9b..2a487a082 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 @@ -73,13 +73,11 @@ object Network : Module( // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets // and posted to the sessionserver api - val (resp, error) = login(mc.session.username, hash) - if (error != null) { - LOG.debug("Unable to authenticate: ${error.message}") - return@listenUnsafe - } - - updateToken(resp!!) + login(mc.session.username, hash) + .fold( + success = { updateToken(it) }, + failure = { LOG.warn("Unable to authenticate: $it") }, + ) } } From aa90562ff9595cbad0a9b12ceb6086e3f4b4e119 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:05:16 -0500 Subject: [PATCH 20/30] Fix capes folder --- common/src/main/kotlin/com/lambda/util/FolderRegister.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 49a1f5442..3ffbf2b07 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -51,7 +51,7 @@ object FolderRegister : Loadable { val structure: Path = lambda.resolve("structure") override fun load(): String { - val folders = listOf(lambda, config, packetLogs, replay, cache, structure) + val folders = listOf(lambda, config, packetLogs, replay, cache, capes, structure) val createdFolders = folders.mapNotNull { if (it.notExists()) { it.createDirectories() From 294361400634343ca62d9d4fbc0596b284ef1d4c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:23:49 -0500 Subject: [PATCH 21/30] Fixed texture downloading --- .../lambda/graphics/texture/TextureUtils.kt | 12 +++++++ .../kotlin/com/lambda/network/CapeManager.kt | 9 +++--- .../com/lambda/network/api/v1/models/Cape.kt | 31 ++++++++++--------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 2fe308fbc..cc712836d 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -71,6 +71,18 @@ object TextureUtils { glPixelStorei(GL_UNPACK_ALIGNMENT, 4) } + fun readImage( + bytes: ByteArray, + format: NativeImage.Format = NativeImage.Format.RGBA, + ): NativeImage { + val buffer = BufferUtils + .createByteBuffer(bytes.size) + .put(bytes) + .flip() + + return NativeImage.read(format, buffer) + } + fun readImage( bufferedImage: BufferedImage, format: NativeImage.Format = NativeImage.Format.RGBA, diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index fc24d9ecd..c0f84c750 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -18,7 +18,6 @@ package com.lambda.network import com.github.kittinunf.fuel.core.FuelError -import com.lambda.Lambda.LOG import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.core.Loadable @@ -56,7 +55,10 @@ object CapeManager : ConcurrentHashMap(), Loadable { fun SafeContext.fetch(uuid: UUID) = getOrPut(uuid) { getCape(uuid) .fold( - success = { if (!images.contains(it.cape)) it.fetch(); put(uuid, it.cape) }, + success = { + if (!images.contains(it.cape)) it.fetch() + put(uuid, it.cape) + }, failure = { throw it }, ) } @@ -65,8 +67,7 @@ object CapeManager : ConcurrentHashMap(), Loadable { init { listen(alwaysListen = true) { - runCatching { fetch(it.uuid) } - .onFailure { LOG.error(it) } + fetch(it.uuid) } } } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt index ab1897d7c..aed9196ce 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt @@ -19,12 +19,16 @@ package com.lambda.network.api.v1.models import com.github.kittinunf.fuel.Fuel import com.google.gson.annotations.SerializedName +import com.lambda.graphics.texture.TextureUtils import com.lambda.sound.SoundManager.toIdentifier import com.lambda.threading.runSafe +import com.lambda.util.Communication.logError import com.lambda.util.FolderRegister.capes import com.lambda.util.extension.resolveFile import net.minecraft.client.texture.NativeImage import net.minecraft.client.texture.NativeImageBackedTexture +import org.lwjgl.BufferUtils +import java.io.BufferedOutputStream import java.io.ByteArrayOutputStream class Cape( @@ -35,20 +39,19 @@ class Cape( val cape: String, ) { fun fetch() = runSafe { - val output = ByteArrayOutputStream(2048*1024*4) - Fuel.download(url) - .streamDestination { _, request -> output to { request.body.toStream() } } - - val image = NativeImage.read(output.toByteArray()) - val native = NativeImageBackedTexture(image) - val id = cape.toIdentifier() - - mc.textureManager.registerTexture(id, native) - - capes.resolveFile("$cape.png") - .writeBytes(output.toByteArray()) + .fileDestination { _, _ -> capes.resolveFile("$cape.png") } + .response { result -> + result.fold( + success = { + val image = TextureUtils.readImage(it) + val native = NativeImageBackedTexture(image) + val id = cape.toIdentifier() + + mc.textureManager.registerTexture(id, native) + }, + failure = { logError("Error while downloading capes", it) } + ) + } } - - fun identifier() = cape.toIdentifier() } From 13e692fe956fc3ac52080ddb3da116d149781c77 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:36:13 -0500 Subject: [PATCH 22/30] Cape feature --- .../lambda/command/commands/CapeCommand.kt | 23 +++++--- .../lambda/module/modules/client/Discord.kt | 9 ++-- .../lambda/module/modules/client/Network.kt | 9 ++-- .../kotlin/com/lambda/network/CapeManager.kt | 53 ++++++++++++++----- .../com/lambda/network/NetworkManager.kt | 8 --- .../network/api/v1/endpoints/GetCape.kt | 9 ++-- .../network/api/v1/endpoints/LinkDiscord.kt | 5 +- .../lambda/network/api/v1/endpoints/Login.kt | 5 +- .../network/api/v1/endpoints/SetCape.kt | 7 +-- .../com/lambda/network/api/v1/models/Cape.kt | 33 ++---------- .../kotlin/com/lambda/util/extension/Other.kt | 6 +++ 11 files changed, 89 insertions(+), 78 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt index 0427ce56d..fe3ee6ff0 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt @@ -17,15 +17,16 @@ package com.lambda.command.commands -import com.lambda.brigadier.CommandResult.Companion.failure import com.lambda.brigadier.CommandResult.Companion.success import com.lambda.brigadier.argument.string import com.lambda.brigadier.argument.value import com.lambda.brigadier.executeWithResult import com.lambda.brigadier.required import com.lambda.command.LambdaCommand +import com.lambda.network.CapeManager.updateCape import com.lambda.network.NetworkManager -import com.lambda.network.api.v1.endpoints.setCape +import com.lambda.threading.runSafe +import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder object CapeCommand : LambdaCommand( @@ -43,12 +44,18 @@ object CapeCommand : LambdaCommand( } executeWithResult { - val cape = id().value() - setCape(cape) - .fold( - success = { NetworkManager.cape = cape; success() }, - failure = { failure(it) }, - ) + runSafe { + val cape = id().value() + + // FixMe: + // try-catch is stupid - + // cannot propagate errors correctly - + // spam the user and fuck off + updateCape(cape) + this@CapeCommand.info("Successfully updated the cape") + + success() + }!! } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 15cb71d1e..536e60e03 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -81,11 +81,10 @@ object Discord : Module( val auth = rpc.applicationManager.authenticate() - linkDiscord(discordToken = auth.accessToken) - .fold( - success = { updateToken(it); discordAuth = auth }, - failure = { warn("Failed to link the discord account to the minecraft auth") }, - ) + linkDiscord(discordToken = auth.accessToken, + success = { updateToken(it); discordAuth = auth }, + failure = { warn("Failed to link the discord account to the minecraft auth") } + ) } private fun stop() { 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 2a487a082..9447e87f3 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 @@ -73,11 +73,10 @@ object Network : Module( // If we log in right as the client responds to the encryption request, we start // a race condition where the game server haven't acknowledged the packets // and posted to the sessionserver api - login(mc.session.username, hash) - .fold( - success = { updateToken(it) }, - failure = { LOG.warn("Unable to authenticate: $it") }, - ) + login(mc.session.username, hash, + success = { updateToken(it) }, + failure = { LOG.warn("Unable to authenticate: $it") } + ) } } diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index c0f84c750..d9fa29d8f 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -17,15 +17,22 @@ package com.lambda.network -import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.requests.CancellableRequest import com.lambda.Lambda.mc import com.lambda.context.SafeContext import com.lambda.core.Loadable import com.lambda.event.events.WorldEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.texture.TextureUtils import com.lambda.network.api.v1.endpoints.getCape +import com.lambda.network.api.v1.endpoints.setCape +import com.lambda.network.api.v1.models.Cape import com.lambda.sound.SoundManager.toIdentifier +import com.lambda.util.Communication.logError 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.util.UUID @@ -47,27 +54,45 @@ object CapeManager : ConcurrentHashMap(), Loadable { .associate { it.nameWithoutExtension to NativeImageBackedTexture(read(it.inputStream())) } .onEach { (key, value) -> mc.textureManager.registerTexture(key.toIdentifier(), value) } + /** + * Sets the current player's cape + */ + fun SafeContext.updateCape(cape: String) = + setCape(cape, + success = { fetchCape(player.uuid) }, + failure = { logError("Could not update the player cape", it) } + )//.join() + /** * Fetches the cape of the given player id - * - * @throws FuelError if something wrong happens */ - fun SafeContext.fetch(uuid: UUID) = getOrPut(uuid) { - getCape(uuid) - .fold( - success = { - if (!images.contains(it.cape)) it.fetch() - put(uuid, it.cape) - }, - failure = { throw it }, - ) - } + fun SafeContext.fetchCape(uuid: UUID): CancellableRequest = + getCape(uuid, + success = { mc.textureManager.get(it.identifier) ?: download(it); put(uuid, it.id) }, + 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 + + mc.textureManager.registerTexture(id, native) + }, + failure = { logError("Could not download the cape", it) } + ) + } override fun load() = "Loaded ${images.size} cached capes" init { listen(alwaysListen = true) { - fetch(it.uuid) + fetchCape(it.uuid) } } } diff --git a/common/src/main/kotlin/com/lambda/network/NetworkManager.kt b/common/src/main/kotlin/com/lambda/network/NetworkManager.kt index 1f483f354..78f38a58a 100644 --- a/common/src/main/kotlin/com/lambda/network/NetworkManager.kt +++ b/common/src/main/kotlin/com/lambda/network/NetworkManager.kt @@ -32,18 +32,10 @@ object NetworkManager : Configurable(UserConfig), Loadable { override val name = "network" var accessToken by setting("authentication", ""); private set - private var _cape by setting("cape", "") val isDiscordLinked: Boolean get() = deserialized?.data?.discordId != null - /** - * Returns the current cape id or null if there are none - */ - var cape: String? = null - get() = _cape.ifEmpty { null } - set(value) { value?.let { CapeManager.put(mc.gameProfile.id, it) }; field = value } - /** * Returns whether the auth has expired */ 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 3a2c2a7ed..323eebe44 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 @@ -18,7 +18,11 @@ 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.ResultHandler import com.github.kittinunf.fuel.gson.responseObject +import com.github.kittinunf.result.failure +import com.github.kittinunf.result.success import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.api.v1.models.Cape @@ -32,7 +36,6 @@ import java.util.UUID * * response: [Cape] or error */ -fun getCape(uuid: UUID) = +fun getCape(uuid: UUID, success: (Cape) -> Unit, failure: (FuelError) -> Unit) = Fuel.get("$apiUrl/api/${apiVersion.value}/cape?id=$uuid") - .responseObject() - .third + .responseObject { _, _, 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 0e4b36b29..3f36e79f0 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 @@ -18,6 +18,7 @@ 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.extensions.authentication import com.github.kittinunf.fuel.core.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject @@ -34,9 +35,9 @@ import com.lambda.network.api.v1.models.Authentication * * response: [Authentication] or error */ -fun linkDiscord(discordToken: String) = +fun linkDiscord(discordToken: String, success: (Authentication) -> Unit, failure: (FuelError) -> Unit) = Fuel.post("${apiUrl}/api/${apiVersion.value}/link/discord") .jsonBody("""{ "token": "$discordToken" }""") .authentication() .bearer(NetworkManager.accessToken) - .responseObject().third + .responseObject { _, _, result -> result.fold(success, failure) } 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 c47e95f02..ecf2c76cd 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 @@ -18,6 +18,7 @@ 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.extensions.jsonBody import com.github.kittinunf.fuel.gson.responseObject import com.lambda.module.modules.client.Network.apiUrl @@ -33,7 +34,7 @@ import com.lambda.network.api.v1.models.Authentication * * response: [Authentication] or error */ -fun login(username: String, hash: String) = +fun login(username: String, hash: String, success: (Authentication) -> Unit, failure: (FuelError) -> Unit) = Fuel.post("${apiUrl}/api/${apiVersion.value}/login") .jsonBody("""{ "username": "$username", "hash": "$hash" }""") - .responseObject().third + .responseObject { _, _, result -> result.fold(success, failure) } 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 ef6457d1b..71af19127 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 @@ -18,8 +18,9 @@ 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.awaitResult import com.github.kittinunf.fuel.core.extensions.authentication -import com.github.kittinunf.fuel.core.responseUnit import com.lambda.module.modules.client.Network.apiUrl import com.lambda.module.modules.client.Network.apiVersion import com.lambda.network.NetworkManager @@ -32,8 +33,8 @@ import com.lambda.network.NetworkManager * * response: [Unit] or error */ -fun setCape(id: String) = +fun setCape(id: String, success: (ByteArray) -> Unit, failure: (FuelError) -> Unit) = Fuel.put("$apiUrl/api/${apiVersion.value}/cape?id=$id") .authentication() .bearer(NetworkManager.accessToken) - .responseUnit().third + .response { _, _, resp -> resp.fold(success, failure) } diff --git a/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt index aed9196ce..041674300 100644 --- a/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt +++ b/common/src/main/kotlin/com/lambda/network/api/v1/models/Cape.kt @@ -17,41 +17,18 @@ package com.lambda.network.api.v1.models -import com.github.kittinunf.fuel.Fuel import com.google.gson.annotations.SerializedName -import com.lambda.graphics.texture.TextureUtils import com.lambda.sound.SoundManager.toIdentifier -import com.lambda.threading.runSafe -import com.lambda.util.Communication.logError -import com.lambda.util.FolderRegister.capes -import com.lambda.util.extension.resolveFile -import net.minecraft.client.texture.NativeImage -import net.minecraft.client.texture.NativeImageBackedTexture -import org.lwjgl.BufferUtils -import java.io.BufferedOutputStream -import java.io.ByteArrayOutputStream +import net.minecraft.util.Identifier +import java.util.UUID class Cape( @SerializedName("url") val url: String, @SerializedName("type") - val cape: String, + val id: String, ) { - fun fetch() = runSafe { - Fuel.download(url) - .fileDestination { _, _ -> capes.resolveFile("$cape.png") } - .response { result -> - result.fold( - success = { - val image = TextureUtils.readImage(it) - val native = NativeImageBackedTexture(image) - val id = cape.toIdentifier() - - mc.textureManager.registerTexture(id, native) - }, - failure = { logError("Error while downloading capes", it) } - ) - } - } + val identifier: Identifier + get() = id.toIdentifier() } 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 36afecef7..979df28f7 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Other.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Other.kt @@ -18,6 +18,9 @@ package com.lambda.util.extension import com.mojang.authlib.GameProfile +import net.minecraft.client.texture.AbstractTexture +import net.minecraft.client.texture.TextureManager +import net.minecraft.util.Identifier import java.io.File import java.nio.file.Path import kotlin.contracts.ExperimentalContracts @@ -35,3 +38,6 @@ val Class<*>.objectInstance: Any fun Path.resolveFile(other: String): File = resolve(other).toFile() + +fun TextureManager.get(identifier: Identifier): AbstractTexture? = + getOrDefault(identifier, null) From cf29f96352a7e2015a3c3a3a7d5debef2730bbca Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:50:30 -0500 Subject: [PATCH 23/30] Fixed missing dependency --- common/build.gradle.kts | 4 +++- common/src/main/kotlin/com/lambda/network/CapeManager.kt | 1 + fabric/build.gradle.kts | 4 +++- forge/build.gradle.kts | 4 +++- gradle.properties | 1 + 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d5a6ba0b5..08af8e3d7 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -22,6 +22,7 @@ val fabricLoaderVersion: String by project val kotlinxCoroutinesVersion: String by project val discordIPCVersion: String by project val fuelVersion: String by project +val resultVersion: String by project base.archivesName = "${base.archivesName.get()}-api" @@ -46,9 +47,10 @@ dependencies { implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") implementation("com.pngencoder:pngencoder:0.15.0") - // Fuel HTTP library + // Fuel HTTP library and dependencies implementation("com.github.kittinunf.fuel:fuel:$fuelVersion") implementation("com.github.kittinunf.fuel:fuel-gson:$fuelVersion") + implementation("com.github.kittinunf.result:result-jvm:$resultVersion") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index d9fa29d8f..d0e96a25a 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -91,6 +91,7 @@ object CapeManager : ConcurrentHashMap(), Loadable { override fun load() = "Loaded ${images.size} cached capes" init { + // FixMe: This works up until the server limit - need to find an alternative listen(alwaysListen = true) { fetchCape(it.uuid) } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index b6bcce2fe..cc392d797 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -23,6 +23,7 @@ val kotlinFabricVersion: String by project val discordIPCVersion: String by project val kotlinVersion: String by project val fuelVersion: String by project +val resultVersion: String by project base.archivesName = "${base.archivesName.get()}-fabric" @@ -89,9 +90,10 @@ dependencies { includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") - // Fuel HTTP library + // Fuel HTTP library and dependencies includeLib("com.github.kittinunf.fuel:fuel:$fuelVersion") includeLib("com.github.kittinunf.fuel:fuel-gson:$fuelVersion") + includeLib("com.github.kittinunf.result:result-jvm:$resultVersion") // Add mods to the mod jar includeMod("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion+$minecraftVersion") diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts index 23c22d278..bec805a76 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -22,6 +22,7 @@ val mixinExtrasVersion: String by project val kotlinForgeVersion: String by project val discordIPCVersion: String by project val fuelVersion: String by project +val resultVersion: String by project base.archivesName = "${base.archivesName.get()}-forge" @@ -100,9 +101,10 @@ dependencies { includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") includeLib("com.pngencoder:pngencoder:0.15.0") - // Fuel HTTP library + // Fuel HTTP library and dependencies includeLib("com.github.kittinunf.fuel:fuel:$fuelVersion") includeLib("com.github.kittinunf.fuel:fuel-gson:$fuelVersion") + includeLib("com.github.kittinunf.result:result-jvm:$resultVersion") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") diff --git a/gradle.properties b/gradle.properties index ec54135cd..86aec0a00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,6 +33,7 @@ javaVersion=17 baritoneVersion=1.10.2 discordIPCVersion=8edf2dbeda fuelVersion=2.3.1 +resultVersion=5.6.0 # Fabric https://fabricmc.net/develop/ fabricLoaderVersion=0.16.9 From 26d28a3417a269777410c9fc15c61f7c3f2b2f59 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:13:36 -0400 Subject: [PATCH 24/30] Update Discord.kt --- .../src/main/kotlin/com/lambda/module/modules/client/Discord.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 536e60e03..3dfcbb0d1 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -57,8 +57,6 @@ object Discord : Module( init { listenOnce { - // If the player is in a party and this most likely means that the `onEnable` - // block ran and is already handling the activity if (rpc.connected) return@listenOnce false runConcurrent { From 21a89e048855f433f9705b012eb6960b94c51e31 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:13:51 -0400 Subject: [PATCH 25/30] Return if no values --- .../main/kotlin/com/lambda/module/modules/client/Network.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 9447e87f3..79d90e1a0 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 @@ -39,6 +39,7 @@ import net.minecraft.network.NetworkSide.CLIENTBOUND import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket import net.minecraft.text.Text import java.math.BigInteger +import kotlin.jvm.optionals.getOrElse object Network : Module( @@ -84,7 +85,7 @@ object Network : Module( val address = ServerAddress.parse(authServer) val connection = ClientConnection(CLIENTBOUND) val resolved = AllowedAddressResolver.DEFAULT.resolve(address) - .map { it.inetSocketAddress }.get() + .map { it.inetSocketAddress }.getOrElse { return } ClientConnection.connect(resolved, mc.options.shouldUseNativeTransport(), connection) .syncUninterruptibly() From acfd1aad31ee5dd01049ab4c86b90d9694f75035 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:34:17 -0400 Subject: [PATCH 26/30] Merge conflict --- common/src/main/kotlin/com/lambda/util/FolderRegister.kt | 2 +- common/src/main/resources/lambda.mixins.common.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 1aebd1822..39044dff7 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -52,7 +52,7 @@ object FolderRegister : Loadable { val maps: Path = lambda.resolve("maps") override fun load(): String { - val folders = listOf(lambda, config, packetLogs, replay, cache, structure) + val folders = listOf(lambda, config, packetLogs, replay, cache, capes, structure, maps) val createdFolders = folders.mapNotNull { if (it.notExists()) { it.createDirectories() diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 29eae63bd..309b2f670 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -14,6 +14,7 @@ "entity.FireworkRocketEntityMixin", "entity.LivingEntityMixin", "entity.PlayerEntityMixin", + "entity.PlayerInventoryMixin", "input.KeyBindingMixin", "input.KeyboardMixin", "input.MouseMixin", From f2fa3574d3af478341e2239cffbf0603004437d2 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:37:59 -0400 Subject: [PATCH 27/30] Removed fix me --- common/src/main/kotlin/com/lambda/network/CapeManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index d0e96a25a..d9fa29d8f 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -91,7 +91,6 @@ object CapeManager : ConcurrentHashMap(), Loadable { override fun load() = "Loaded ${images.size} cached capes" init { - // FixMe: This works up until the server limit - need to find an alternative listen(alwaysListen = true) { fetchCape(it.uuid) } From b5d18964d575dbbd8de5883da979ca1a73877643 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:56:48 -0400 Subject: [PATCH 28/30] Fixed hash utils --- .../module/modules/player/MapDownloader.kt | 3 ++- .../kotlin/com/lambda/util/StringUtils.kt | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) 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 74a5da185..2d8854f19 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 @@ -24,6 +24,7 @@ import com.lambda.module.tag.ModuleTag import com.lambda.util.FolderRegister import com.lambda.util.FolderRegister.locationBoundDirectory import com.lambda.util.StringUtils.hash +import com.lambda.util.StringUtils.hashString import com.lambda.util.player.SlotUtils.combined import com.lambda.util.world.entitySearch import net.minecraft.block.MapColor @@ -57,7 +58,7 @@ object MapDownloader : Module( } private val MapState.hash: String - get() = colors.hash("SHA-256") + get() = colors.hashString("SHA-256") fun MapState.toBufferedImage(): BufferedImage { val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB) diff --git a/common/src/main/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt index 0af9eb225..177c545c7 100644 --- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -101,11 +101,21 @@ object StringUtils { * * @return The string representation of the hash */ - fun String.hash(algorithm: String, vararg extra: ByteArray): String = - MessageDigest - .getInstance(algorithm) - .apply { update(toByteArray()); extra.forEach(::update) } - .digest() + fun String.hashString(algorithm: String, vararg extra: ByteArray): String = + toByteArray().hash(algorithm, *extra) + .joinToString(separator = "") { "%02x".format(it) } + + /** + * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification + * + * @receiver The byte array to hash + * @param algorithm The algorithm instance to use + * @param extra Additional data to digest with the byte array + * + * @return The string representation of the hash + */ + fun ByteArray.hashString(algorithm: String, vararg extra: ByteArray): String = + hash(algorithm, *extra) .joinToString(separator = "") { "%02x".format(it) } /** @@ -113,7 +123,7 @@ object StringUtils { * * @receiver The byte array to hash * @param algorithm The algorithm instance to use - * @param extra Additional data to digest with the bytearray + * @param extra Additional data to digest with the byte array * * @return The digested data */ From d79aac4f62f21be43e2e492c05256b3033f337a8 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Mar 2025 17:57:10 -0400 Subject: [PATCH 29/30] Unused import --- .../kotlin/com/lambda/module/modules/player/MapDownloader.kt | 1 - 1 file changed, 1 deletion(-) 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 2d8854f19..dc44bf8f5 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 @@ -23,7 +23,6 @@ import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.FolderRegister import com.lambda.util.FolderRegister.locationBoundDirectory -import com.lambda.util.StringUtils.hash import com.lambda.util.StringUtils.hashString import com.lambda.util.player.SlotUtils.combined import com.lambda.util.world.entitySearch From aa4206f38fecf8d9cb7713abf955b011355b84ba Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:11:08 -0400 Subject: [PATCH 30/30] Log error instead of propagate --- .../lambda/command/commands/CapeCommand.kt | 37 ++++++++----------- .../kotlin/com/lambda/network/CapeManager.kt | 7 ++-- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt index fe3ee6ff0..6214ff182 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt @@ -17,16 +17,15 @@ package com.lambda.command.commands -import com.lambda.brigadier.CommandResult.Companion.success +import com.lambda.brigadier.argument.literal import com.lambda.brigadier.argument.string import com.lambda.brigadier.argument.value -import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand import com.lambda.network.CapeManager.updateCape import com.lambda.network.NetworkManager import com.lambda.threading.runSafe -import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder object CapeCommand : LambdaCommand( @@ -35,27 +34,21 @@ object CapeCommand : LambdaCommand( description = "Sets your cape", ) { override fun CommandBuilder.create() { - required(string("id")) { id -> - suggests { _, builder -> - NetworkManager.capes - .forEach { builder.suggest(it) } + required(literal("set")) { + required(string("id")) { id -> + suggests { _, builder -> + NetworkManager.capes + .forEach { builder.suggest(it) } - builder.buildFuture() - } - - executeWithResult { - runSafe { - val cape = id().value() - - // FixMe: - // try-catch is stupid - - // cannot propagate errors correctly - - // spam the user and fuck off - updateCape(cape) - this@CapeCommand.info("Successfully updated the cape") + builder.buildFuture() + } - success() - }!! + execute { + runSafe { + val cape = id().value() + updateCape(cape) + } + } } } } diff --git a/common/src/main/kotlin/com/lambda/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt index d9fa29d8f..93873b3c4 100644 --- a/common/src/main/kotlin/com/lambda/network/CapeManager.kt +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -29,6 +29,7 @@ import com.lambda.network.api.v1.endpoints.getCape import com.lambda.network.api.v1.endpoints.setCape 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.FolderRegister.capes import com.lambda.util.extension.get @@ -57,11 +58,11 @@ object CapeManager : ConcurrentHashMap(), Loadable { /** * Sets the current player's cape */ - fun SafeContext.updateCape(cape: String) = + fun SafeContext.updateCape(cape: String): CancellableRequest = setCape(cape, - success = { fetchCape(player.uuid) }, + success = { fetchCape(player.uuid); info("Successfully update your cape to $cape") }, failure = { logError("Could not update the player cape", it) } - )//.join() + ) /** * Fetches the cape of the given player id