diff --git a/.gitignore b/.gitignore index b427213a2..887ee74c4 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ logs/ ### Architectury ### .architectury-transformer/ + +### Kotlin ### +*.kotlin_module +.kotlin/ diff --git a/build.gradle.kts b/build.gradle.kts index 148e45a27..1d966875b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,11 +62,11 @@ allprojects { base.archivesName = modId repositories { - maven("https://api.modrinth.com/maven") - maven("https://jitpack.io") - maven("https://maven.shedaniel.me/") { name = "Architectury" } + maven("https://maven.shedaniel.me/") // Architectury maven("https://maven.terraformersmc.com/releases/") - maven("https://babbaj.github.io/maven/") + maven("https://babbaj.github.io/maven/") // Baritone + maven("https://jitpack.io") // KDiscordIPC + mavenCentral() // Allow the use of local libraries flatDir { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 1d9d133b9..c918e1820 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,6 +2,7 @@ val modId: String by project val fabricLoaderVersion: String by project val kotlinVersion: String by project val kotlinxCoroutinesVersion: String by project +val discordIPCVersion: String by project architectury { common("fabric", "forge", "neoforge") } @@ -21,6 +22,7 @@ dependencies { // Add dependencies on the required Kotlin modules. implementation("org.reflections:reflections:0.10.2") + implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") // Add Kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") diff --git a/common/src/main/java/com/lambda/mixin/ClientConnectionMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientConnectionMixin.java similarity index 96% rename from common/src/main/java/com/lambda/mixin/ClientConnectionMixin.java rename to common/src/main/java/com/lambda/mixin/network/ClientConnectionMixin.java index bed77b462..1bdba743f 100644 --- a/common/src/main/java/com/lambda/mixin/ClientConnectionMixin.java +++ b/common/src/main/java/com/lambda/mixin/network/ClientConnectionMixin.java @@ -1,4 +1,4 @@ -package com.lambda.mixin; +package com.lambda.mixin.network; import com.lambda.event.EventFlow; import com.lambda.event.events.ConnectionEvent; @@ -73,7 +73,7 @@ private void onConnect( ConnectionIntent intent, CallbackInfo ci ) { - EventFlow.post(new ConnectionEvent.Connect(address, port, listener, intent)); + EventFlow.post(new ConnectionEvent.Connect.Pre(address, port, listener, intent)); } @Inject(method = "disconnect(Lnet/minecraft/text/Text;)V", at = @At("HEAD")) diff --git a/common/src/main/java/com/lambda/mixin/network/ClientLoginNetworkMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientLoginNetworkMixin.java new file mode 100644 index 000000000..4b183a560 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/network/ClientLoginNetworkMixin.java @@ -0,0 +1,22 @@ +package com.lambda.mixin.network; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.ConnectionEvent; +import com.lambda.module.modules.client.DiscordRPC; +import net.minecraft.client.network.ClientLoginNetworkHandler; +import net.minecraft.network.packet.s2c.login.LoginSuccessS2CPacket; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientLoginNetworkHandler.class) +public class ClientLoginNetworkMixin { + + @Inject(method = "onSuccess(Lnet/minecraft/network/packet/s2c/login/LoginSuccessS2CPacket;)V", at = @At("HEAD")) + private void onSuccess(LoginSuccessS2CPacket packet, CallbackInfo ci) { + EventFlow.post(new ConnectionEvent.Connect.Post(packet.getProfile())); + } +} diff --git a/common/src/main/java/com/lambda/mixin/network/HandshakeC2SPacketMixin.java b/common/src/main/java/com/lambda/mixin/network/HandshakeC2SPacketMixin.java new file mode 100644 index 000000000..5218e76a8 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/network/HandshakeC2SPacketMixin.java @@ -0,0 +1,18 @@ +package com.lambda.mixin.network; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.ConnectionEvent; +import net.minecraft.network.packet.c2s.handshake.ConnectionIntent; +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(HandshakeC2SPacket.class) +public class HandshakeC2SPacketMixin { + @Inject(method = "(ILjava/lang/String;ILnet/minecraft/network/packet/c2s/handshake/ConnectionIntent;)V", at = @At("TAIL")) + private void onHandshakeC2SPacket(int i, String string, int j, ConnectionIntent connectionIntent, CallbackInfo ci) { + EventFlow.post(new ConnectionEvent.Connect.Handshake(i, string, j, connectionIntent)); + } +} diff --git a/common/src/main/java/com/lambda/mixin/network/LoginHelloC2SPacketMixin.java b/common/src/main/java/com/lambda/mixin/network/LoginHelloC2SPacketMixin.java new file mode 100644 index 000000000..67667f0a3 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/network/LoginHelloC2SPacketMixin.java @@ -0,0 +1,19 @@ +package com.lambda.mixin.network; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.ConnectionEvent; +import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.UUID; + +@Mixin(LoginHelloC2SPacket.class) +public class LoginHelloC2SPacketMixin { + @Inject(method = "(Ljava/lang/String;Ljava/util/UUID;)V", at = @At("TAIL")) + private void onLoginHelloC2SPacket(String string, UUID uUID, CallbackInfo ci) { + EventFlow.post(new ConnectionEvent.Connect.Login.Hello(string, uUID)); + } +} diff --git a/common/src/main/java/com/lambda/mixin/network/LoginKeyC2SPacketMixin.java b/common/src/main/java/com/lambda/mixin/network/LoginKeyC2SPacketMixin.java new file mode 100644 index 000000000..22247df80 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/network/LoginKeyC2SPacketMixin.java @@ -0,0 +1,22 @@ +package com.lambda.mixin.network; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.ConnectionEvent; +import net.minecraft.network.packet.c2s.login.LoginKeyC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.crypto.SecretKey; +import java.security.PublicKey; + +@Mixin(LoginKeyC2SPacket.class) +public class LoginKeyC2SPacketMixin { + @Inject(method = "(Ljavax/crypto/SecretKey;Ljava/security/PublicKey;[B)V", at = @At("TAIL")) + private void onLoginKeyC2SPacket(SecretKey secretKey, PublicKey publicKey, byte[] nonce, CallbackInfo ci) { + // Please note this won't work if the server is offline mode because the player doesn't + // fetch the server's public key. + EventFlow.post(new ConnectionEvent.Connect.Login.Key(secretKey, publicKey, nonce)); + } +} diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index 8605202ab..5248d5852 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -26,6 +26,7 @@ object Lambda { const val MOD_NAME = "Lambda" const val MOD_ID = "lambda" const val SYMBOL = "λ" + const val APP_ID = "1221289599427416127" val VERSION: String = LoaderInfo.getVersion() val LOG: Logger = LogManager.getLogger(SYMBOL) diff --git a/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt new file mode 100644 index 000000000..750ec1669 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt @@ -0,0 +1,31 @@ +package com.lambda.command.commands + +import com.lambda.brigadier.* +import com.lambda.brigadier.argument.literal +import com.lambda.brigadier.argument.value +import com.lambda.brigadier.argument.word +import com.lambda.command.LambdaCommand +import com.lambda.module.modules.client.DiscordRPC +import com.lambda.util.extension.CommandBuilder + +object RpcCommand : LambdaCommand( + name = "rpc", + description = "Discord Rich Presence commands.", + usage = "rpc " +) { + override fun CommandBuilder.create() { + required(literal("join")) { + required(word("id")) { id -> + execute { + DiscordRPC.join(id().value()) + } + } + } + + required(literal("accept")) { + execute { + DiscordRPC.join() + } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/event/events/ConnectionEvent.kt b/common/src/main/kotlin/com/lambda/event/events/ConnectionEvent.kt index 4bdba55bc..bcff6e801 100644 --- a/common/src/main/kotlin/com/lambda/event/events/ConnectionEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/ConnectionEvent.kt @@ -1,17 +1,93 @@ package com.lambda.event.events import com.lambda.event.Event +import com.mojang.authlib.GameProfile import net.minecraft.network.listener.PacketListener import net.minecraft.network.packet.c2s.handshake.ConnectionIntent import net.minecraft.text.Text +import java.security.PublicKey +import java.util.UUID +import javax.crypto.SecretKey -abstract class ConnectionEvent : Event { - class Connect( - val host: String, - val port: Int, - val listener: PacketListener, - val intent: ConnectionIntent, - ) : ConnectionEvent() +/** + * Sealed class representing connection events. + */ +sealed class ConnectionEvent : Event { + /** + * Sealed class representing various stages of connection establishment. + */ + sealed class Connect { + /** + * Event representing a pre-connection attempt. + * @property address The address of the connection attempt. + * @property port The port of the connection attempt. + * @property listener The packet listener associated with the connection. + * @property intent The connection intent. + */ + class Pre( + val address: String, + val port: Int, + val listener: PacketListener, + val intent: ConnectionIntent, + ) : ConnectionEvent() + /** + * Event representing a handshake during connection. + * @property protocolVersion The protocol version of the connection. + * @property address The address of the connection attempt. + * @property port The port of the connection attempt. + * @property intent The connection intent. + */ + class Handshake( + val protocolVersion: Int, + val address: String, + val port: Int, + val intent: ConnectionIntent, + ) : ConnectionEvent() + + /** + * Sealed class representing login-related connection events. + */ + sealed class Login : ConnectionEvent() { + /** + * Event representing a hello message during login. + * @property name The name associated with the login. + * @property uuid The UUID associated with the login. + */ + class Hello( + val name: String, + val uuid: UUID, + ) : ConnectionEvent() + + /** + * Event representing the exchange of cryptographic keys during login. + * @property secretKey The secret key exchanged during login. + * @property publicKey The public key exchanged during login. + * @property nonce The nonce associated with the login. + * + * The secret key MUST ABSOLUTELY be, if stored, destroyed after use to avoid security vulnerabilities. + * This can be done by calling the `destroy()` method on the secret key object. + * We are NOT responsible for any incidents that may occur due to improper handling of cryptographic keys. + */ + class Key( + val secretKey: SecretKey, + val publicKey: PublicKey, + val nonce: ByteArray, + ) : ConnectionEvent() + } + + /** + * Event representing post-connection actions. + * @property profile The game profile associated with the connection. + */ + class Post( + val profile: GameProfile, + ) : ConnectionEvent() + } + + /** + * Event representing a disconnection. + * @property reason The reason for disconnection. + */ class Disconnect(val reason: Text) : ConnectionEvent() } diff --git a/common/src/main/kotlin/com/lambda/http/Extensions.kt b/common/src/main/kotlin/com/lambda/http/Extensions.kt new file mode 100644 index 000000000..a347b5381 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/Extensions.kt @@ -0,0 +1,34 @@ +package com.lambda.http + +import com.lambda.Lambda +import java.net.URLEncoder + +/** + * Extension property to convert a map to a URL query string. + */ +val Map.query: String + get() = map { (key, value) -> "$key=${value.urlEncoded}" }.joinToString("&") + +/** + * Extension property to URL encode a string. + */ +val Any.urlEncoded: String get() = URLEncoder.encode(toString(), "UTF-8") + +/** + * Extension function to convert a map to a JSON string. + */ +fun Map.toJson(): String = Lambda.gson.toJson(this) + +/** + * Try-catch block wrapped with a default value. + */ +fun tryOrDefault(default: T, block: () -> T): T = try { + block() +} catch (e: Exception) { + default +} + +/** + * Try-catch block wrapped with null + */ +fun tryOrNull(block: () -> T): T? = tryOrDefault(null, block) diff --git a/common/src/main/kotlin/com/lambda/http/Method.kt b/common/src/main/kotlin/com/lambda/http/Method.kt new file mode 100644 index 000000000..db9d76817 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/Method.kt @@ -0,0 +1,15 @@ +package com.lambda.http + +/** + * Enum representing HTTP methods. + */ +enum class Method { + GET, + HEAD, + POST, + PUT, + DELETE, + OPTIONS, + TRACE, + PATCH; +} diff --git a/common/src/main/kotlin/com/lambda/http/Request.kt b/common/src/main/kotlin/com/lambda/http/Request.kt new file mode 100644 index 000000000..f975fa0ff --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/Request.kt @@ -0,0 +1,129 @@ +package com.lambda.http + +import com.lambda.Lambda +import com.lambda.util.FolderRegister.cache +import java.io.File +import java.net.HttpURLConnection +import java.net.URL +import java.time.Instant +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days + +/** + * Represents an HTTP request. + * + * @property url The URL to which the request will be made. + * @property method The HTTP method to be used for the request. Default is [Method.GET]. + * @property parameters A map of query parameters to be included in the request. Default is an empty map. + * @property headers A map of headers to be included in the request. Default is an empty map. + * @property config A lambda function to configure the HTTP connection. Default is an empty lambda. + */ +data class Request( + val url: String, + val method: Method = Method.GET, + val parameters: Map = mapOf(), + val headers: Map = mapOf(), + val config: ((HttpURLConnection) -> Unit) = {}, +) { + val exceptionFormat = "HTTP request failed with status code %d\nResponse: %s" + + val canBeEncoded: Boolean + get() = method != Method.POST && method != Method.PUT && method != Method.PATCH + + /** + * Downloads the resource at the specified path and caches it for future use. + * + * @param path The path to the resource. + * @param maxAge The maximum age of the cached resource. Default is 4 days. + */ + fun maybeDownload(path: String, maxAge: Duration = 4.days): ByteArray { + val file = File("${cache}/${path.substringAfterLast("/").hashCode()}") + + if (file.exists() && Instant.now().toEpochMilli() - file.lastModified() < maxAge.inWholeMilliseconds) + return file.readBytes() + + file.writeText("") // Clear the file before writing to it. + + val url = URL( + if (parameters.isNotEmpty() && canBeEncoded) "$url?${parameters.query}" + else url + ) + + val connection = url.openConnection() as HttpURLConnection + config.invoke(connection) + + connection.requestMethod = method.name + + headers.forEach { (key, value) -> connection.setRequestProperty(key, value) } + + connection.connect() + + connection.inputStream.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + + return file.readBytes() + } + + /** + * Executes the HTTP request synchronously. + */ + inline fun json(): Response { + val url = URL( + if (parameters.isNotEmpty() && canBeEncoded) "$url?${parameters.query}" + else url + ) + + val connection = url.openConnection() as HttpURLConnection + config.invoke(connection) + + connection.requestMethod = method.name + + headers.forEach { (key, value) -> connection.setRequestProperty(key, Lambda.gson.toJson(value)) } + + // For the moment we are only supporting JSON requests. + connection.setRequestProperty("Content-Type", "application/json") + + runCatching { + if (!canBeEncoded) { + connection.doOutput = true + connection.outputStream.use { + it.write(parameters.toJson().toByteArray()) + } + } else { + connection.connect() + } + }.onFailure { + return Response( + connection = connection, + data = null, + error = it + ) + } + + if (connection.responseCode !in 200..299) { + return Response( + connection = connection, + data = null, + error = Throwable( + exceptionFormat.format( + connection.responseCode, + tryOrDefault( + "A critical error causes the remote client to abruptly close the connection.\n" + + "No action is required on your side." + ) { + connection.errorStream.bufferedReader().readText() + } + ) + ) + ) + } + + return Response( + connection = connection, + data = tryOrNull { Lambda.gson.fromJson(connection.inputStream.bufferedReader().readText(), Success::class.java) }, + ) + } +} diff --git a/common/src/main/kotlin/com/lambda/http/RequestBuilder.kt b/common/src/main/kotlin/com/lambda/http/RequestBuilder.kt new file mode 100644 index 000000000..d380a4847 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/RequestBuilder.kt @@ -0,0 +1,59 @@ +package com.lambda.http + +import java.net.HttpURLConnection + +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +@DslMarker +annotation class RequestDsl + +@RequestDsl +class RequestBuilder( + private val url: String, +) { + private var method: Method = Method.GET + private val parameters: MutableMap = mutableMapOf() + private val headers: MutableMap = mutableMapOf() + private var config: ((HttpURLConnection) -> Unit) = {} + + /** + * Sets the HTTP method to be used for the request. + */ + fun method(method: Method): RequestBuilder { + this.method = method + return this + } + + /** + * Sets the query parameters to be included in the request. + */ + fun parameters(parameters: Map): RequestBuilder { + this.parameters.putAll(parameters) + return this + } + + /** + * Sets the headers to be included in the request. + */ + fun headers(headers: Map): RequestBuilder { + this.headers.putAll(headers) + return this + } + + /** + * Sets the lambda function to configure the HTTP connection. + */ + fun config(config: (HttpURLConnection) -> Unit): RequestBuilder { + this.config = config + return this + } + + fun build() = Request(url, method, parameters, headers, config) +} + +/** + * Creates an HTTP request. + * + * @param url The URL to which the request will be made. + * @param block A lambda function to configure the request. + */ +inline fun request(url: String, block: (@RequestDsl RequestBuilder).() -> Unit) = RequestBuilder(url).apply(block).build() diff --git a/common/src/main/kotlin/com/lambda/http/Response.kt b/common/src/main/kotlin/com/lambda/http/Response.kt new file mode 100644 index 000000000..9654e6657 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/Response.kt @@ -0,0 +1,29 @@ +package com.lambda.http + +import java.net.HttpURLConnection + +/** + * Represents an HTTP response. + */ +class Response( + /** + * The response + */ + var data: Success? = null, + + /** + * The error + */ + var error: Throwable? = null, + + /** + * The HTTP connection associated with the response. + */ + var connection: HttpURLConnection? = null, +) { + /** + * Indicates whether the request was successful (HTTP status code 2xx). + */ + val success: Boolean + get() = connection?.let { return it.responseCode in 200..299 } ?: false +} diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt new file mode 100644 index 000000000..d6e1b15aa --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.kt @@ -0,0 +1,35 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun createParty( + endpoint: String, + version: String, + accessToken: String, + + // 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, +) = + request("$endpoint/api/$version/party/create") { + method(Method.POST) + + parameters( + mapOf( + "max_players" to maxPlayers, + "public" to public, + ) + ) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt new file mode 100644 index 000000000..6a124ae9d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt @@ -0,0 +1,19 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun deleteParty( + endpoint: String, + version: String, + accessToken: String, +) = + request("$endpoint/api/$version/party/delete") { + method(Method.DELETE) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt new file mode 100644 index 000000000..eb12d3a1e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt @@ -0,0 +1,19 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun createParty( + endpoint: String, + version: String, + accessToken: String, +) = + request("$endpoint/api/$version/party") { + method(Method.POST) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt new file mode 100644 index 000000000..e91d207db --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt @@ -0,0 +1,29 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Request +import com.lambda.http.Method +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun joinParty( + endpoint: String, + version: String, + accessToken: String, + + // The ID of the party. + // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" + partyId: String, +) = + request("$endpoint/api/$version/party/join") { + method(Method.PUT) + + parameters( + mapOf( + "id" to partyId, + ) + ) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt new file mode 100644 index 000000000..6a9a67a75 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt @@ -0,0 +1,19 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun leaveParty( + endpoint: String, + version: String, + accessToken: String, +) = + request("$endpoint/api/$version/party/leave") { + method(Method.PUT) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() 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 new file mode 100644 index 000000000..1912c61d6 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/Login.kt @@ -0,0 +1,34 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Authentication +import com.lambda.http.request + +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 +) = + request("$endpoint/api/$version/login") { + method(Method.POST) + + parameters( + mapOf( + "token" to discordToken, + "username" to username, + "hash" to hash + ) + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt new file mode 100644 index 000000000..6a8e3d85a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt @@ -0,0 +1,35 @@ +package com.lambda.http.api.rpc.v1.endpoints + +import com.lambda.http.Method +import com.lambda.http.Request +import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.http.request + +fun editParty( + endpoint: String, + version: String, + accessToken: String, + + // 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, +) = + request("$endpoint/api/$version/party/edit") { + method(Method.PATCH) + + parameters( + mapOf( + "max_players" to maxPlayers, + "public" to public, + ) + ) + + headers( + mapOf("Authorization" to "Bearer $accessToken") + ) + }.json() diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt new file mode 100644 index 000000000..0a22d4254 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Authentication.kt @@ -0,0 +1,20 @@ +package com.lambda.http.api.rpc.v1.models + +import com.google.gson.annotations.SerializedName + +data class Authentication( + // The access token to use for the API + // example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + @SerializedName("access_token") + val accessToken: String, + + // The duration of the token (in seconds). + // example: 3600 + @SerializedName("expires_in") + val expiresIn: Long, + + // The type of the token. + // example: Bearer + @SerializedName("token_type") + val tokenType: String, +) diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt new file mode 100644 index 000000000..0636170fa --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt @@ -0,0 +1,32 @@ +package com.lambda.http.api.rpc.v1.models + +import com.google.gson.annotations.SerializedName + +data class Party( + // The ID of the party. + // It is a random string of 30 characters. + @SerializedName("id") + val id: String, + + // 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, + + // The settings of the party + @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/http/api/rpc/v1/models/Player.kt new file mode 100644 index 000000000..acfc17ad6 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Player.kt @@ -0,0 +1,20 @@ +package com.lambda.http.api.rpc.v1.models + +import com.google.gson.annotations.SerializedName + +data class Player ( + // The player's name. + // example: Notch + @SerializedName("name") + val name: String, + + // The player's UUID. + // example: 069a79f4-44e9-4726-a5be-fca90e38aaf5 + @SerializedName("uuid") + val uuid: String, + + // The player's Discord ID. + // example: "385441179069579265" + @SerializedName("discord_id") + val discordId: String, +) diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt new file mode 100644 index 000000000..bb7c53cc7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Settings.kt @@ -0,0 +1,16 @@ +package com.lambda.http.api.rpc.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, +) diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt b/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt new file mode 100644 index 000000000..18d9f06f8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt @@ -0,0 +1,262 @@ +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.event.events.ConnectionEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.listener.UnsafeListener.Companion.unsafeListener +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.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.onShutdown +import com.lambda.threading.runConcurrent +import com.lambda.util.Communication.warn +import com.lambda.util.Nameable +import com.lambda.util.StringUtils.capitalize +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.network.encryption.NetworkEncryptionUtils +import net.minecraft.network.packet.s2c.login.LoginHelloS2CPacket +import java.math.BigInteger +import java.util.concurrent.atomic.AtomicReference + +object DiscordRPC : Module( + name = "DiscordRPC", + description = "Discord Rich Presence configuration", + defaultTags = setOf(ModuleTag.CLIENT), + enabledByDefault = true, +) { + private val page by setting("Page", Page.General) + + /* General settings */ + 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 } + + private val confirmCoordinates by setting("Show Coordinates", false, description = "Confirm display the player coordinates") { page == Page.General } + private val confirmServer by setting("Expose server", false, description = "Confirm display the server IP") { page == Page.General } + private val showTime by setting("Show Time", true, description = "Show how long you have been playing for.") { page == Page.General } + + /* Technical settings */ + private var rpcServer by setting("RPC Server", "http://127.0.0.1:8080") { page == Page.Settings } // TODO: Change this in production + private var apiVersion by setting("API Version", ApiVersion.V1) { page == Page.Settings } + private val delay by setting("Update Delay", 5000, 5000..10000, 1, unit = "ms", visibility = { page == Page.Settings }) + + /* Party settings */ + private val enableParty by setting("Enable Party", true, description = "Allows you to create parties.") { page == Page.Party } + private val createByDefault by setting("Create Party by Default", true, description = "Automatically create a party when you join a server.") { page == Page.Party && enableParty } + private val maxPlayers by setting("Max Players", 10, 2..20, visibility = { page == Page.Party }).apply { onValueChange { _, _ -> edit() } } + private val public by setting("Public Party", false, description = "Allow anyone to join your party.") { page == Page.Party }.apply { onValueChange { _, _ -> edit() } } + + private val rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) + private var startup = System.currentTimeMillis() + private val dimensionRegex = Regex("""\b\w+_\w+\b""") + + private var discordAuth: AuthenticatePacket.Data? = null + private var rpcAuth: Authentication? = null + private var currentParty: AtomicReference = AtomicReference(null) + + private var connectionTime: Long = 0 + private var serverId: String? = null + + /** + * If the player can interact with the party system. + */ + private val allowed: Boolean + get() = rpcAuth != null && discordAuth != null && enableParty + + private enum class Page { + General, Settings, Party + } + + private enum class LineInfo(val value: () -> String) : Nameable { + VERSION({ Lambda.VERSION }), + WORLD({ + when { + mc.currentServerEntry != null -> "Multiplayer" + mc.isIntegratedServerRunning -> "Singleplayer" + else -> "Main Menu" + } + }), + 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]" + }), + 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"), + } + + init { + unsafeListener { + if (it.packet !is LoginHelloS2CPacket) return@unsafeListener + connectionTime = System.currentTimeMillis() + serverId = it.packet.serverId + } + + // Will not work if the player doesn't have a valid key pair + // from Mojang's authentication server. + unsafeListener { event -> + runConcurrent { + connect(event) + } + } + + onEnableUnsafe { + runConcurrent { + connect() + } + } + + onDisableUnsafe { disconnect() } + onShutdown { disconnect() } + } + + private suspend fun connect(event: ConnectionEvent.Connect.Login.Key? = null) { + startup = System.currentTimeMillis() + + if (event != null) rpc.register(event) + if (!rpc.connected) runConcurrent { rpc.connect() } + + while (true) { + if (rpc.connected) update() + delay(delay.toLong()) + } + } + + private fun disconnect() { + if (rpc.connected) { + LOG.info("Gracefully disconnecting from Discord RPC.") + rpc.disconnect() + leaveParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return) + } + } + + fun join(id: String = rpc.activityManager.activity?.party?.id ?: "") { + if (!allowed) return + + joinParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return, id) + .also { response -> + if (response.error != null) warn("Failed to join the party: ${response.error}") + currentParty.lazySet(response.data) + } + } + + private fun edit() { + if (!allowed) return + + currentParty.acquire?.let { + editParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return, maxPlayers, public) + .also { response -> + if (response.error != null) warn("Failed to edit the party: ${response.error}") + currentParty.lazySet(response.data) + } + } + } + + private suspend fun update() { + val party = currentParty.acquire + + rpc.activityManager.setActivity { + details = "${line1Left.value()} | ${line1Right.value()}".take(128) + state = "${line2Left.value()} | ${line2Right.value()}".take(128) + + largeImage("lambda", Lambda.VERSION) + smallImage("https://mc-heads.net/avatar/${mc.gameProfile.id}/nohelm", mc.gameProfile.name) + + if (allowed && party != null) { + party(party.id, party.players.size, party.settings.maxPlayers) + secrets(party.joinSecret) + } else { + button("Download", "https://github.com/lambda-client/lambda") + } + + if (showTime) timestamps(startup) + } + } + + private suspend fun KDiscordIPC.register(auth: ConnectionEvent.Connect.Login.Key) { + // TODO: Check if the rpc is already ready + on { + // Party features + subscribe(DiscordEvent.ActivityJoinRequest) + subscribe(DiscordEvent.ActivityJoin) + //subscribe(DiscordEvent.LobbyUpdate) + //subscribe(DiscordEvent.LobbyDelete) + //subscribe(DiscordEvent.LobbyMemberConnect) + //subscribe(DiscordEvent.LobbyMemberDisconnect) + //subscribe(DiscordEvent.LobbyMemberUpdate) + + // QOL features + //subscribe(DiscordEvent.SpeakingStart) + //subscribe(DiscordEvent.SpeakingStop) + + 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() + + login(rpcServer, apiVersion.value, discordAuth?.accessToken ?: "", mc.session.username, hash) + .also { response -> + if (response.error != null) warn("Failed to authenticate with the RPC server: ${response.error}") + rpcAuth = response.data + } + + if (createByDefault) createParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return@on, maxPlayers, public) + .also { response -> + if (response.error != null) warn("Failed to create a party: ${response.error}") + currentParty.lazySet(response.data) + } + } + + // Event when someone would like to join your party + on { + LOG.info("Received a party join request from ${data.userId}.") + rpc.activityManager.acceptJoinRequest(data.userId) + // TODO: Implement a GUI for this + } + + // Event when someone joins your party + on { + LOG.info("TEst invite join") + } + + on { + LOG.error("Discord RPC error: ${data.message}") + } + } +} diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index 414c00ee0..9102b8db6 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -56,7 +56,7 @@ object GuiSettings : Module( } exp({ targetScale }, 0.5).apply { - unsafeListener(alwaysListen = true) { + unsafeListener(alwaysListen = true) { setValue(targetScale) } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt index bfe0976b6..99f71b5b7 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/BackTrack.kt @@ -146,7 +146,7 @@ object BackTrack : Module( event.cancel() } - listener { + listener { receivePool.clear() sendPool.clear() } @@ -188,4 +188,4 @@ object BackTrack : Module( connection.sendPacketSilently(packet) } } -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/XRay.kt b/common/src/main/kotlin/com/lambda/module/modules/render/XRay.kt index c1597ead0..2211d9931 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/render/XRay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/render/XRay.kt @@ -23,15 +23,15 @@ object XRay : Module( Blocks.ANCIENT_DEBRIS ) - private val selection by setting("Block Selection", defaultBlocks, "Block selection that will be shown (whitelist) or hidden (blacklist)") + //private val selection by setting("Block Selection", defaultBlocks, "Block selection that will be shown (whitelist) or hidden (blacklist)") private val mode by setting("Selection Mode", Selection.WHITELIST, "The mode of the block selection") @JvmStatic fun isSelected(blockState: BlockState) = mode.select(blockState) enum class Selection(val select: (BlockState) -> Boolean) { - WHITELIST({ it.block in selection }), - BLACKLIST({ it.block !in selection }) + WHITELIST({ it.block in com.lambda.module.modules.render.XRay.defaultBlocks }), + BLACKLIST({ it.block !in com.lambda.module.modules.render.XRay.defaultBlocks }) } init { diff --git a/common/src/main/kotlin/com/lambda/threading/Hook.kt b/common/src/main/kotlin/com/lambda/threading/Hook.kt new file mode 100644 index 000000000..ab77b0715 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/threading/Hook.kt @@ -0,0 +1,64 @@ +package com.lambda.threading + +import kotlin.concurrent.thread + +/** + * Registers a shutdown hook to execute the specified [block] of code when the application is shutting down. + * This function ensures that the given [block] will be executed before the JVM terminates, allowing graceful cleanup + * or finalization tasks to be performed. + * + * @param block the code to be executed on shutdown. + * + * ## Usage: + * ``` + * onShutdown { + * // Perform cleanup tasks or finalizations here + * println("Shutting down gracefully...") + * } + * ``` + * + * ## Advantages: + * - Provides a convenient and reliable mechanism for executing cleanup tasks when the application exits. + * - Helps ensure that critical resources are released properly, preventing resource leaks or data corruption. + * - Can be used to handle cleanup in various scenarios, such as closing open connections, saving application state, + * or logging shutdown events. + * + * ## Code Examples: + * 1. Registering a simple shutdown hook: + * ``` + * onShutdown { + * println("Performing cleanup...") + * } + * ``` + * 2. Performing resource cleanup on shutdown: + * ``` + * onShutdown { + * databaseConnection.close() + * fileWriter.close() + * println("Resources closed successfully.") + * } + * ``` + * + * ## What Not to Do: + * - Avoid performing time-consuming or blocking operations inside the shutdown hook, as it may delay the shutdown process + * and lead to undesirable behavior. + * - Do not rely solely on shutdown hooks for critical tasks that must be executed reliably. Consider using other + * mechanisms such as proper exception handling or manual cleanup where necessary. + * - Avoid registering multiple shutdown hooks for the same purpose, as it may result in unexpected behavior or conflicts + * between the hooks. + * + * ## Edge Cases: + * - If the JVM is terminated abruptly or forcefully (e.g., via `kill -9` on Unix-like systems), the shutdown hooks may + * not have a chance to run, leading to potential resource leaks or incomplete cleanup. This is a rare scenario but + * should be considered when designing critical cleanup tasks. + * - In certain environments or configurations where the JVM shutdown process is interrupted or bypassed, such as when + * running in a containerized environment where the container itself is forcibly stopped, the shutdown hooks may not + * execute as expected. It's important to be aware of the behavior of the runtime environment and handle such cases + * accordingly, possibly by implementing additional cleanup mechanisms or relying on external systems for graceful + * shutdown coordination. + */ +inline fun onShutdown(crossinline block: () -> Unit) { + Runtime.getRuntime().addShutdownHook(thread(start = false) { block() }) +} + + diff --git a/common/src/main/kotlin/com/lambda/threading/Threading.kt b/common/src/main/kotlin/com/lambda/threading/Threading.kt index 445b50b75..05e04d093 100644 --- a/common/src/main/kotlin/com/lambda/threading/Threading.kt +++ b/common/src/main/kotlin/com/lambda/threading/Threading.kt @@ -131,4 +131,4 @@ inline fun runSafeGameConcurrent(crossinline block: SafeContext.() -> Unit) { * @param block The task to be executed on the game's main thread within a safe context. */ suspend inline fun awaitMainThread(noinline block: SafeContext.() -> T) = - CompletableFuture.supplyAsync({ runSafe { block() } }, mc).await() ?: throw IllegalStateException("Unsafe") \ No newline at end of file + CompletableFuture.supplyAsync({ runSafe { block() } }, mc).await() ?: throw IllegalStateException("Unsafe") diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index ee6ffc8e6..26af87db9 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -28,6 +28,7 @@ object FolderRegister { val config: File = File(lambda, "config") val packetLogs: File = File(lambda, "packet-log") val replay: File = File(lambda, "replay") + val cache: File = File(lambda, "cache") fun File.createIfNotExists() { if (!exists()) { diff --git a/common/src/main/kotlin/com/lambda/util/extension/Player.kt b/common/src/main/kotlin/com/lambda/util/extension/Player.kt new file mode 100644 index 000000000..38db84194 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/extension/Player.kt @@ -0,0 +1,10 @@ +package com.lambda.util.extension + +import com.mojang.authlib.GameProfile +import net.minecraft.entity.player.PlayerEntity + +val PlayerEntity.isOffline + get() = gameProfile.isOffline + +val GameProfile.isOffline + get() = properties.isEmpty diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index 6fe70d93d..f4a2a1a28 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -55,6 +55,8 @@ accessible field net/minecraft/network/packet/s2c/play/EntityS2CPacket id I accessible method net/minecraft/network/ClientConnection handlePacket (Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;)V accessible field net/minecraft/network/ClientConnection packetsSentCounter I 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 # Other accessible field net/minecraft/world/explosion/Explosion behavior Lnet/minecraft/world/explosion/ExplosionBehavior; diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index d9c7a6fb4..d39928708 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -4,7 +4,6 @@ "package": "com.lambda.mixin", "compatibilityLevel": "JAVA_17", "client": [ - "ClientConnectionMixin", "MinecraftClientMixin", "baritone.MixinBaritonePlayerContext", "baritone.MixinLookBehavior", @@ -18,6 +17,11 @@ "input.KeyboardMixin", "items.BarrierBlockMixin", "items.TridentMixin", + "network.ClientConnectionMixin", + "network.ClientLoginNetworkMixin", + "network.HandshakeC2SPacketMixin", + "network.LoginHelloC2SPacketMixin", + "network.LoginKeyC2SPacketMixin", "render.BlockRenderManagerMixin", "render.CameraMixin", "render.ChatInputSuggestorMixin", diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 2eb3c1876..29198c218 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -3,6 +3,7 @@ val minecraftVersion: String by project val fabricLoaderVersion: String by project val fabricApiVersion: String by project val kotlinFabricVersion: String by project +val discordIPCVersion: String by project base.archivesName = "${base.archivesName.get()}-fabric" @@ -65,6 +66,7 @@ dependencies { includeLib("org.reflections:reflections:0.10.2") includeLib("org.javassist:javassist:3.28.0-GA") includeLib("dev.babbaj:nether-pathfinder:1.5") + includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") // 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 09c8a71ef..3c56a065b 100644 --- a/forge/build.gradle.kts +++ b/forge/build.gradle.kts @@ -3,6 +3,7 @@ val minecraftVersion: String by project val forgeVersion: String by project val mixinExtrasVersion: String by project val kotlinForgeVersion: String by project +val discordIPCVersion: String by project base.archivesName = "${base.archivesName.get()}-forge" @@ -17,7 +18,6 @@ architectury { loom { accessWidenerPath.set(project(":common").loom.accessWidenerPath) - forge { // This is required to convert the access wideners to the forge // format, access transformers. @@ -39,6 +39,8 @@ repositories { // you can add it to the `settings.gradle.kts` file // in the base of the project and gradle will do the // rest for you. + // If you want to add more global repositories, you can + // add them to the root build.gradle.kts file. maven("https://thedarkcolour.github.io/KotlinForForge/") } @@ -61,12 +63,14 @@ fun DependencyHandlerScope.setupConfigurations() { includeLib.dependencies.forEach { implementation(it) forgeRuntimeLibrary(it) + implementation(it) include(it) } includeMod.dependencies.forEach { modImplementation(it) forgeRuntimeLibrary(it) + implementation(it) include(it) } } @@ -79,6 +83,14 @@ dependencies { includeLib("org.reflections:reflections:0.10.2") includeLib("org.javassist:javassist:3.28.0-GA") + // Temporary, only works for production + // See https://github.com/MinecraftForge/MinecraftForge/issues/8878 + shadowBundle("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") { + exclude(group = "org.jetbrains.kotlin") + exclude(group = "org.jetbrains.kotlinx") + exclude(group = "org.slf4j") + } + // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge:$kotlinForgeVersion") includeMod("baritone-api:baritone-unoptimized-forge:1.10.2") diff --git a/gradle.properties b/gradle.properties index 4e19f1cb1..9d515b4aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,7 @@ mixinExtrasVersion=0.4.1 kotlinVersion=2.0.0 kotlinxCoroutinesVersion=1.9.0-RC javaVersion=17 +discordIPCVersion=15b3373013 # Fabric https://fabricmc.net/develop/ fabricLoaderVersion=0.15.11 diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index 4786c1fbf..38f15a3c7 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -2,6 +2,7 @@ val modVersion: String by project val minecraftVersion: String by project val neoVersion: String by project val kotlinForgeVersion: String by project +val discordIPCVersion: String by project base.archivesName = "${base.archivesName.get()}-neoforge" @@ -27,8 +28,8 @@ repositories { // you can add it to the `settings.gradle.kts` file // in the base of the project and gradle will do the // rest for you. - maven("https://maven.neoforged.net/releases/") maven("https://thedarkcolour.github.io/KotlinForForge/") + maven("https://maven.neoforged.net/releases/") } val common: Configuration by configurations.creating { @@ -65,6 +66,8 @@ dependencies { // Add dependencies on the required Kotlin modules. includeLib("org.reflections:reflections:0.10.2") includeLib("org.javassist:javassist:3.28.0-GA") + includeLib("dev.babbaj:nether-pathfinder:1.5") + includeLib("com.github.Edouard127:KDiscordIPC:$discordIPCVersion") // Add mods to the mod jar includeMod("thedarkcolour:kotlinforforge-neoforge:$kotlinForgeVersion") diff --git a/settings.gradle.kts b/settings.gradle.kts index ca0590866..1811a0cd4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ pluginManagement { maven("https://maven.fabricmc.net/") maven("https://maven.architectury.dev/") maven("https://maven.minecraftforge.net/") + maven("https://jitpack.io") mavenCentral() gradlePluginPortal() }