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/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/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java index 81d615c4d..fb43c434d 100644 --- a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java +++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java @@ -20,7 +20,11 @@ import com.lambda.event.EventFlow; import com.lambda.event.events.InventoryEvent; import com.lambda.module.modules.render.NoRender; +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; @@ -31,6 +35,24 @@ @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 = "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 new file mode 100644 index 000000000..309978183 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/CapeFeatureRendererMixin.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.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; + +@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, 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/java/com/lambda/mixin/world/ClientWorldMixin.java b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java index 877548369..37d039efe 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,6 +37,8 @@ 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 = "addEntity", at = @At("HEAD"), cancellable = true) diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt index 8e79152c2..9806d9891 100644 --- a/common/src/main/kotlin/com/lambda/Lambda.kt +++ b/common/src/main/kotlin/com/lambda/Lambda.kt @@ -47,6 +47,8 @@ object Lambda { @JvmStatic val mc: MinecraftClient by lazy { MinecraftClient.getInstance() } + val isDebug = System.getProperty("lambda.dev") != null + val gson: Gson = GsonBuilder() .setPrettyPrinting() .registerTypeAdapter(KeyCode::class.java, KeyCodeSerializer) diff --git a/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt rename to common/src/main/kotlin/com/lambda/command/commands/CapeCommand.kt index 1de9513db..6214ff182 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/RpcCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/CapeCommand.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 @@ -18,24 +18,36 @@ package com.lambda.command.commands import com.lambda.brigadier.argument.literal +import com.lambda.brigadier.argument.string 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.DiscordRPC +import com.lambda.network.CapeManager.updateCape +import com.lambda.network.NetworkManager +import com.lambda.threading.runSafe import com.lambda.util.extension.CommandBuilder -object RpcCommand : LambdaCommand( - name = "rpc", - description = "Discord Rich Presence commands.", - usage = "rpc " -) { +object CapeCommand : LambdaCommand( + name = "cape", + usage = "set ", + description = "Sets your cape", +) { override fun CommandBuilder.create() { - required(literal("join")) { - required(word("id")) { id -> + required(literal("set")) { + required(string("id")) { id -> + suggests { _, builder -> + NetworkManager.capes + .forEach { builder.suggest(it) } + + builder.buildFuture() + } + execute { - DiscordRPC.join(id().value()) + runSafe { + val cape = id().value() + updateCape(cape) + } } } } diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt b/common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt similarity index 64% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt rename to common/src/main/kotlin/com/lambda/config/configurations/UserConfig.kt index 786c59084..c3279e24b 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/DeleteParty.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/UserConfig.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 @@ -15,15 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.config.configurations -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.config.Configuration +import com.lambda.util.FolderRegister +import java.io.File -fun deleteParty( - endpoint: String, - version: String, -) = - Fuel.delete("$endpoint/api/$version/party/delete") - .responseObject().third +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 202ab03f9..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. @@ -36,6 +39,31 @@ import net.minecraft.world.chunk.WorldChunk * occurrences in the game world. */ sealed class WorldEvent { + // ToDo: Add doc and determine if there's a better place for this event + // 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/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/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt index 16eca5626..8a4545585 100644 --- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt +++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt @@ -50,6 +50,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, 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 deleted file mode 100644 index fae958bf2..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/CreateParty.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.Party - -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, -) = - Fuel.post("$endpoint/api/$version/party/create", listOf( - "max_players" to maxPlayers, - "public" to public, - )).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/http/api/rpc/v1/endpoints/JoinParty.kt deleted file mode 100644 index 25dc8c291..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/JoinParty.kt +++ /dev/null @@ -1,34 +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.Party - -fun joinParty( - endpoint: String, - version: String, - accessToken: String, - - // The ID of the party. - // example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" - partyId: String, -) = - Fuel.put("$endpoint/api/$version/party/join", listOf("id" to partyId)) - .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/http/api/rpc/v1/endpoints/LeaveParty.kt deleted file mode 100644 index 8763924db..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/LeaveParty.kt +++ /dev/null @@ -1,30 +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.Party - -fun leaveParty( - endpoint: String, - version: String, - accessToken: String, -) = - Fuel.put("$endpoint/api/$version/party/leave") - .responseObject().third 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/http/api/rpc/v1/endpoints/UpdateParty.kt b/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt deleted file mode 100644 index 25f38f82c..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/UpdateParty.kt +++ /dev/null @@ -1,39 +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.Party - -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, -) = - Fuel.patch("$endpoint/api/$version/party/edit", listOf("max_players" to maxPlayers)) - .responseObject().third 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 deleted file mode 100644 index f2011d039..000000000 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/models/Party.kt +++ /dev/null @@ -1,50 +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.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, - - // The settings of the party - @SerializedName("settings") - val settings: Settings, -) diff --git a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt similarity index 63% rename from common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt rename to common/src/main/kotlin/com/lambda/module/modules/client/Capes.kt index f957b7095..e56d19edc 100644 --- a/common/src/main/kotlin/com/lambda/http/api/rpc/v1/endpoints/GetParty.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Capes.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 @@ -15,16 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.http.api.rpc.v1.endpoints +package com.lambda.module.modules.client -import com.github.kittinunf.fuel.Fuel -import com.github.kittinunf.fuel.gson.responseObject -import com.lambda.http.api.rpc.v1.models.Party +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag -fun createParty( - endpoint: String, - version: String, - accessToken: String, -) = - Fuel.get("$endpoint/api/$version/party") - .responseObject().third +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 new file mode 100644 index 000000000..3dfcbb0d1 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -0,0 +1,121 @@ +/* + * 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.module.modules.client + +import com.lambda.Lambda +import com.lambda.context.SafeContext +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.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 +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.packet.inbound.impl.AuthenticatePacket +import dev.cbyrne.kdiscordipc.data.activity.* +import kotlinx.coroutines.delay + +object Discord : Module( + name = "Discord", + description = "Discord Rich Presence configuration", + defaultTags = setOf(ModuleTag.CLIENT), + //enabledByDefault = true, // ToDo: Bring this back on beta release +) { + 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 + + init { + listenOnce { + if (rpc.connected) return@listenOnce false + + runConcurrent { + start() + handleLoop() + } + + return@listenOnce true + } + + onEnable { runConcurrent { start(); handleLoop() } } + onDisable { stop() } + } + + private suspend fun start() { + if (rpc.connected) return + + runConcurrent { rpc.connect() } + delay(1000) + + val auth = rpc.applicationManager.authenticate() + + linkDiscord(discordToken = auth.accessToken, + success = { updateToken(it); discordAuth = auth }, + failure = { warn("Failed to link the discord account to the minecraft auth") } + ) + } + + private fun stop() { + if (rpc.connected) rpc.disconnect() + } + + private suspend fun SafeContext.handleLoop() { + while (rpc.connected) { + update() + delay(delay) + } + } + + private suspend fun SafeContext.update() { + 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) + button("Download", "https://github.com/lambda-client/lambda") + + if (showTime) timestamps(startup) + } + } + + private enum class LineInfo(val value: SafeContext.() -> String) : Nameable { + VERSION({ Lambda.VERSION }), + WORLD({ worldName }), + USERNAME({ mc.session.username }), + 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/DiscordRPC.kt b/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt deleted file mode 100644 index 24d216775..000000000 --- a/common/src/main/kotlin/com/lambda/module/modules/client/DiscordRPC.kt +++ /dev/null @@ -1,279 +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.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.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.module.Module -import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runConcurrent -import com.lambda.util.Communication.logError -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.entity.player.PlayerEntity -import net.minecraft.network.encryption.NetworkEncryptionUtils -import java.math.BigInteger - -object DiscordRPC : Module( - name = "DiscordRPC", - description = "Discord Rich Presence configuration", - defaultTags = setOf(ModuleTag.CLIENT), -// enabledByDefault = true, // ToDo: Bring this back on beta release -) { - private val page by setting("Page", Page.General) - - /* General settings */ - 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 } - 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 rpc = KDiscordIPC(Lambda.APP_ID, scope = EventFlow.lambdaScope) - private var startup = System.currentTimeMillis() - private val dimensionRegex = Regex("""\b\w+_\w+\b""") - - 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 - - private val PlayerEntity.isPartyOwner - get() = uuid == currentParty?.leader?.uuid - - private 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 - } - - listenUnsafe { connect() } - - // TODO: Exponential backoff up to 25 seconds - onEnable { connect() } - onDisable { disconnect() } - } - - 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 - - currentParty = party - } - - // Join a party using the ID - 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()) - - currentParty = party - } - - // Edit the current party if you are the owner - 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()) - - currentParty = party - } - - private fun connect() { - runConcurrent { rpc.connect() } - - runConcurrent { - keyEvent?.let { rpc.register(it) } - } - - runConcurrent { - while (rpc.connected) { - updateActivity() - delay(delay) - } - } - } - - private fun disconnect() { - if (rpc.connected) { - LOG.info("Gracefully disconnecting from Discord RPC.") - leaveParty(rpcServer, apiVersion.value, rpcAuth?.accessToken ?: return) - rpc.disconnect() - } - - ready = null - discordAuth = null - rpcAuth = null - currentParty = null - keyEvent = null - } - - private suspend fun updateActivity() { - val party = currentParty - - 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 (isPartyInteractionAllowed && party != null) { - party(party.id.toString(), 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.EncryptionResponse) { - on { - ready = this - - // Party features - 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") - 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, 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"), - } -} 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..79d90e1a0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Network.kt @@ -0,0 +1,103 @@ +/* + * 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.Lambda.LOG +import com.lambda.Lambda.mc +import com.lambda.event.events.ClientEvent +import com.lambda.event.events.ConnectionEvent +import com.lambda.event.events.ConnectionEvent.Connect.Login.EncryptionResponse +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.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.packet.c2s.login.LoginHelloC2SPacket +import net.minecraft.text.Text +import java.math.BigInteger +import kotlin.jvm.optionals.getOrElse + + +object Network : Module( + name = "Network", + description = "Lambda Authentication", + 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) + + private lateinit var hash: String + + init { + listenUnsafeConcurrently { authenticate() } + + listenUnsafe { event -> + if (event.secretKey.isDestroyed) return@listenUnsafe + + // 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) + } + + 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 + // and posted to the sessionserver api + login(mc.session.username, hash, + success = { updateToken(it) }, + failure = { LOG.warn("Unable to authenticate: $it") } + ) + } + } + + private fun authenticate() { + val address = ServerAddress.parse(authServer) + val connection = ClientConnection(CLIENTBOUND) + val resolved = AllowedAddressResolver.DEFAULT.resolve(address) + .map { it.inetSocketAddress }.getOrElse { return } + + ClientConnection.connect(resolved, mc.options.shouldUseNativeTransport(), connection) + .syncUninterruptibly() + + val handler = ClientLoginNetworkHandler(connection, mc, null, null, false, null) { Text.empty() } + + connection.connect(resolved.hostName, resolved.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/module/modules/player/MapDownloader.kt b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt index 74a5da185..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,7 @@ 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 import net.minecraft.block.MapColor @@ -57,7 +57,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/network/CapeManager.kt b/common/src/main/kotlin/com/lambda/network/CapeManager.kt new file mode 100644 index 000000000..93873b3c4 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/CapeManager.kt @@ -0,0 +1,99 @@ +/* + * 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.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.info +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 +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) } + + /** + * Sets the current player's cape + */ + fun SafeContext.updateCape(cape: String): CancellableRequest = + setCape(cape, + success = { fetchCape(player.uuid); info("Successfully update your cape to $cape") }, + failure = { logError("Could not update the player cape", it) } + ) + + /** + * Fetches the cape of the given player id + */ + 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) { + 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 new file mode 100644 index 000000000..78f38a58a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/NetworkManager.kt @@ -0,0 +1,77 @@ +/* + * 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 + + val isDiscordLinked: Boolean + get() = deserialized?.data?.discordId != null + + /** + * 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 new file mode 100644 index 000000000..323eebe44 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/GetCape.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.network.api.v1.endpoints + +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.core.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 +import java.util.UUID + +/** + * Gets the cape of the given player UUID + * + * Example: + * - id: ab24f5d6-dcf1-45e4-897e-b50a7c5e7422 + * + * response: [Cape] or error + */ +fun getCape(uuid: UUID, success: (Cape) -> Unit, failure: (FuelError) -> Unit) = + Fuel.get("$apiUrl/api/${apiVersion.value}/cape?id=$uuid") + .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 new file mode 100644 index 000000000..3f36e79f0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/LinkDiscord.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.network.api.v1.endpoints + +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.core.extensions.authentication +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.NetworkManager +import com.lambda.network.api.v1.models.Authentication + +/** + * Links a Discord account to a session account + * + * Example: + * - token: OTk1MTU1NzcyMzYxMTQ2NDM4 + * + * response: [Authentication] or error + */ +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 { _, _, 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 new file mode 100644 index 000000000..ecf2c76cd --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/Login.kt @@ -0,0 +1,40 @@ +/* + * 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.FuelError +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 + +/** + * Creates a new session account with mojang session hashes + * + * Example: + * - username: Notch + * - hash: 069a79f444e94726a5befca90e38aaf5 + * + * response: [Authentication] or error + */ +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 { _, _, 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 new file mode 100644 index 000000000..71af19127 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/network/api/v1/endpoints/SetCape.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.network.api.v1.endpoints + +import com.github.kittinunf.fuel.Fuel +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.core.awaitResult +import com.github.kittinunf.fuel.core.extensions.authentication +import com.lambda.module.modules.client.Network.apiUrl +import com.lambda.module.modules.client.Network.apiVersion +import com.lambda.network.NetworkManager + +/** + * Sets the currently authenticated player's cape + * + * Example: + * - id: galaxy + * + * response: [Unit] or error + */ +fun setCape(id: String, success: (ByteArray) -> Unit, failure: (FuelError) -> Unit) = + Fuel.put("$apiUrl/api/${apiVersion.value}/cape?id=$id") + .authentication() + .bearer(NetworkManager.accessToken) + .response { _, _, resp -> resp.fold(success, failure) } 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 80% 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..7dfb3a6a8 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 @@ -34,4 +34,18 @@ data class Authentication( // example: Bearer @SerializedName("token_type") val tokenType: String, -) +) { + data class Data( + @SerializedName("nbf") + val notBefore: Long, + + @SerializedName("iat") + val issuedAt: Long, + + @SerializedName("exp") + val expirationDate: Long, + + @SerializedName("data") + val data: Player, + ) +} 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/Cape.kt similarity index 64% 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/Cape.kt index 96f07a806..041674300 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/Cape.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 @@ -15,19 +15,20 @@ * 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 com.lambda.sound.SoundManager.toIdentifier +import net.minecraft.util.Identifier +import java.util.UUID -data class Settings( - // The maximum number of players in the party. - // example: 10 - @SerializedName("max_players") - val maxPlayers: Int, +class Cape( + @SerializedName("url") + val url: String, - // Whether the party is public or not. - // If false can only be joined by invite. - // example: true - // @SerializedName("public") - // val public: Boolean, -) + @SerializedName("type") + val id: String, +) { + val identifier: Identifier + get() = id.toIdentifier() +} 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/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 28348f60a..39044dff7 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -47,11 +47,12 @@ 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") val maps: Path = lambda.resolve("maps") override fun load(): String { - val folders = listOf(lambda, config, packetLogs, replay, cache, structure, maps) + 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/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt index 28eaa1894..177c545c7 100644 --- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -97,13 +97,12 @@ object StringUtils { * * @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): String = - MessageDigest - .getInstance(algorithm) - .digest(toByteArray()) + fun String.hashString(algorithm: String, vararg extra: ByteArray): String = + toByteArray().hash(algorithm, *extra) .joinToString(separator = "") { "%02x".format(it) } /** @@ -111,12 +110,26 @@ object StringUtils { * * @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.hash(algorithm: String): String = + fun ByteArray.hashString(algorithm: String, vararg extra: ByteArray): String = + 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 digested data + */ + fun ByteArray.hash(algorithm: String, vararg extra: ByteArray): ByteArray = MessageDigest .getInstance(algorithm) - .digest(this) - .joinToString(separator = "") { "%02x".format(it) } + .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..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,11 @@ 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 import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -30,3 +35,9 @@ 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() + +fun TextureManager.get(identifier: Identifier): AbstractTexture? = + getOrDefault(identifier, null) 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 diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index b4559b085..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", @@ -29,10 +30,12 @@ "render.BlockRenderManagerMixin", "render.CameraMixin", "render.ChatHudMixin", + "render.CapeFeatureRendererMixin", "render.ChatInputSuggestorMixin", "render.ChatScreenMixin", "render.DebugHudMixin", "render.DrawContextMixin", + "render.ElytraFeatureRendererMixin", "render.GameRendererMixin", "render.GlStateManagerMixin", "render.InGameHudMixin", @@ -56,8 +59,5 @@ ], "injectors": { "defaultRequire": 1 - }, - "mixins": [ - "entity.PlayerInventoryMixin" - ] + } } 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 51613fab3..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" @@ -35,7 +36,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. @@ -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 dd7d3f16f..86aec0a00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,8 +31,9 @@ kotlinVersion=2.0.20 kotlinxCoroutinesVersion=1.9.0-RC javaVersion=17 baritoneVersion=1.10.2 -discordIPCVersion=7ab2e77312 +discordIPCVersion=8edf2dbeda fuelVersion=2.3.1 +resultVersion=5.6.0 # Fabric https://fabricmc.net/develop/ fabricLoaderVersion=0.16.9