diff --git a/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt index c5c456648..68ac65dd0 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt @@ -29,7 +29,7 @@ import com.lambda.util.extension.CommandBuilder object ConfigCommand : LambdaCommand( name = "config", aliases = setOf("cfg"), - usage = "config ", + usage = "config ", description = "Save or load the configuration files" ) { override fun CommandBuilder.create() { diff --git a/common/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt new file mode 100644 index 000000000..c97fbac69 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/FriendCommand.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.command.commands + +import com.lambda.Lambda.mc +import com.lambda.brigadier.CommandResult.Companion.failure +import com.lambda.brigadier.CommandResult.Companion.success +import com.lambda.brigadier.argument.literal +import com.lambda.brigadier.argument.string +import com.lambda.brigadier.argument.uuid +import com.lambda.brigadier.argument.value +import com.lambda.brigadier.execute +import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.required +import com.lambda.command.LambdaCommand +import com.lambda.config.configurations.FriendConfig +import com.lambda.friend.FriendManager +import com.lambda.util.Communication.info +import com.lambda.util.extension.CommandBuilder +import com.lambda.util.text.ClickEvents +import com.lambda.util.text.buildText +import com.lambda.util.text.literal +import com.lambda.util.text.styled +import java.awt.Color + +object FriendCommand : LambdaCommand( + name = "friends", + usage = "friends ", + description = "Add or remove a friend" +) { + override fun CommandBuilder.create() { + execute { + this@FriendCommand.info( + buildText { + if (FriendManager.friends.isEmpty()) { + literal("You have no friends yet. Go make some! :3\n") + } else { + literal("Your friends (${FriendManager.friends.size}):\n") + + FriendManager.friends.forEachIndexed { index, gameProfile -> + literal(" ${index + 1}. ${gameProfile.name}\n") + } + } + + literal("\n") + styled( + color = Color.CYAN, + underlined = true, + clickEvent = ClickEvents.openFile(FriendConfig.primary.path), + ) { + literal("Click to open your friends list as a file") + } + } + ) + } + + required(literal("add")) { + required(string("player name")) { player -> + suggests { _, builder -> + mc.networkHandler + ?.playerList + ?.filter { it.profile != mc.gameProfile } + ?.map { it.profile.name } + ?.forEach { builder.suggest(it) } + + builder.buildFuture() + } + + executeWithResult { + val name = player().value() + val id = mc.networkHandler + ?.playerList + ?.firstOrNull { + it.profile.name == name && + it.profile != mc.gameProfile + } ?: return@executeWithResult failure("Could not find the player on the server") + + return@executeWithResult if (FriendManager.befriend(id.profile)) { + this@FriendCommand.info(FriendManager.befriendedText(id.profile.name)) + success() + } else { + failure("This player is already in your friend list") + } + } + } + + required(uuid("player uuid")) { player -> + suggests { _, builder -> + mc.networkHandler + ?.playerList + ?.filter { it.profile != mc.gameProfile } + ?.map { it.profile.id } + ?.forEach { builder.suggest(it.toString()) } + + builder.buildFuture() + } + + executeWithResult { + val uuid = player().value() + val id = mc.networkHandler + ?.playerList + ?.firstOrNull { + it.profile.id == uuid && it.profile != mc.gameProfile + } ?: return@executeWithResult failure("Could not find the player on the server") + + return@executeWithResult if (FriendManager.befriend(id.profile)) { + this@FriendCommand.info(FriendManager.befriendedText(id.profile.name)) + success() + } else { + failure("This player is already in your friend list") + } + } + } + } + + required(literal("remove")) { + required(string("player name")) { player -> + suggests { _, builder -> + FriendManager.friends.map { it.name } + .forEach { builder.suggest(it) } + + builder.buildFuture() + } + + executeWithResult { + val name = player().value() + val profile = FriendManager.gameProfile(name) + ?: return@executeWithResult failure("This player is not in your friend list") + + return@executeWithResult if (FriendManager.unfriend(profile)) { + this@FriendCommand.info(FriendManager.unfriendedText(name)) + success() + } else { + failure("This player is not in your friend list") + } + } + } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt index 29f3fbea2..f5610f997 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -33,7 +33,7 @@ import com.lambda.util.extension.CommandBuilder object ReplayCommand : LambdaCommand( name = "replay", - usage = "replay ", + usage = "replay ", description = "Play, load, save, or prune a replay" ) { override fun CommandBuilder.create() { diff --git a/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt index 67a4b932b..4ab9ab51f 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt @@ -32,7 +32,7 @@ import com.lambda.util.extension.CommandBuilder object TransferCommand : LambdaCommand( name = "transfer", - usage = "transfer ", + usage = "transfer ", description = "Transfer items from anywhere to anywhere", ) { private var lastTransfer: TransferResult.Transfer? = null diff --git a/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt b/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt index 1950dc46c..e54586340 100644 --- a/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/AbstractSetting.kt @@ -100,6 +100,10 @@ abstract class AbstractSetting( value = gson.fromJson(serialized, type) } + /** + * Will only register changes of the variable, not the content of the variable! + * E.g., if the variable is a list, it will only register if the list reference changes, not if the content of the list changes. + */ fun onValueChange(block: SafeContext.(from: T, to: T) -> Unit) { listeners.add(ValueListener(true) { from, to -> runSafe { diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index 4f388e91d..3a5c38429 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -248,7 +248,6 @@ abstract class Configurable( * @param name The unique identifier for the setting. * @param defaultValue The default [Set] value of type [T] for the setting. * @param description A brief explanation of the setting's purpose and behavior. - * @param hackDelegates A flag that determines whether the setting should be serialized with the default value. * @param visibility A lambda expression that determines the visibility status of the setting. * * ```kotlin @@ -262,14 +261,12 @@ abstract class Configurable( name: String, defaultValue: Set, description: String = "", - hackDelegates: Boolean = false, noinline visibility: () -> Boolean = { true }, ) = SetSetting( name, defaultValue.toMutableSet(), TypeToken.getParameterized(MutableSet::class.java, T::class.java).type, description, - hackDelegates, visibility, ).also { settings.add(it) diff --git a/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt index eec3047d0..160256c1b 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/collections/SetSetting.kt @@ -17,8 +17,6 @@ package com.lambda.config.settings.collections -import com.google.gson.JsonElement -import com.lambda.Lambda.gson import com.lambda.config.AbstractSetting import java.lang.reflect.Type @@ -27,27 +25,13 @@ import java.lang.reflect.Type */ class SetSetting( override val name: String, - private val defaultValue: MutableSet, + defaultValue: MutableSet, type: Type, description: String, - private val hackDelegates: Boolean, visibility: () -> Boolean, ) : AbstractSetting>( defaultValue, type, description, visibility -) { - override fun toJson(): JsonElement { - return if (hackDelegates) gson.toJsonTree(defaultValue, type) - else super.toJson() - } - - override fun loadFromJson(serialized: JsonElement) { - if (hackDelegates) { - defaultValue.addAll(gson.fromJson(serialized, type)) - setValue(this, ::value, defaultValue.distinct().toMutableSet()) - } - else super.loadFromJson(serialized) - } -} +) diff --git a/common/src/main/kotlin/com/lambda/friend/FriendManager.kt b/common/src/main/kotlin/com/lambda/friend/FriendManager.kt index 306b1e310..aa57de7a6 100644 --- a/common/src/main/kotlin/com/lambda/friend/FriendManager.kt +++ b/common/src/main/kotlin/com/lambda/friend/FriendManager.kt @@ -20,35 +20,68 @@ package com.lambda.friend import com.lambda.config.Configurable import com.lambda.config.configurations.FriendConfig import com.lambda.core.Loadable +import com.lambda.util.text.* import com.mojang.authlib.GameProfile import net.minecraft.client.network.OtherClientPlayerEntity +import net.minecraft.text.Text +import java.awt.Color import java.util.* +// ToDo: +// - Allow adding of offline players by name or uuid. +// - Should store the data until the player was seen. +// Either no UUID but with name or no name but with uuid or both. +// -> Should update the record if the player was seen again. +// - Handle player changing names. +// - Improve save file structure. object FriendManager : Configurable(FriendConfig), Loadable { override val name = "friends" - val friends by setting("friends", listOf(), hackDelegates = true) + val friends by setting("friends", setOf()) - fun add(profile: GameProfile) { if (!contains(profile)) friends.add(profile) } + fun befriend(profile: GameProfile) = friends.add(profile) + fun unfriend(profile: GameProfile): Boolean = friends.remove(profile) - fun remove(profile: GameProfile) { friends.remove(profile) } + fun gameProfile(name: String) = friends.firstOrNull { it.name == name } + fun gameProfile(uuid: UUID) = friends.firstOrNull { it.id == uuid } - fun get(name: String) = friends.firstOrNull { it.name == name } - fun get(uuid: UUID) = friends.firstOrNull { it.id == uuid } - - fun contains(profile: GameProfile) = friends.contains(profile) - fun contains(name: String) = friends.any { it.name == name } - fun contains(uuid: UUID) = friends.any { it.id == uuid } + fun isFriend(profile: GameProfile) = friends.contains(profile) + fun isFriend(name: String) = friends.any { it.name == name } + fun isFriend(uuid: UUID) = friends.any { it.id == uuid } fun clear() = friends.clear() val OtherClientPlayerEntity.isFriend: Boolean - get() = contains(gameProfile) + get() = isFriend(gameProfile) - fun OtherClientPlayerEntity.befriend() = add(gameProfile) - fun OtherClientPlayerEntity.unfriend() = remove(gameProfile) + fun OtherClientPlayerEntity.befriend() = befriend(gameProfile) + fun OtherClientPlayerEntity.unfriend() = unfriend(gameProfile) override fun load(): String { // TODO: Because the settings are loaded after the property and the loadables, the friend list is empty at that point return "Loaded ${friends.size} friends" } + + fun befriendedText(name: String): Text = befriendedText(Text.of(name)) + fun befriendedText(name: Text) = buildText { + literal(Color.GREEN, "Added ") + text(name) + literal(" to your friend list ") + clickEvent(ClickEvents.suggestCommand(";friends remove ${name.string}")) { + styled(underlined = true, color = Color.LIGHT_GRAY) { + literal("[Undo]") + } + } + } + + fun unfriendedText(name: String): Text = unfriendedText(Text.of(name)) + fun unfriendedText(name: Text) = buildText { + literal(Color.RED, "Removed ") + text(name) + literal(" from your friend list ") + clickEvent(ClickEvents.suggestCommand(";friends add ${name.string}")) { + styled(underlined = true, color = Color.LIGHT_GRAY) { + literal("[Undo]") + } + } + } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt b/common/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt new file mode 100644 index 000000000..6de55cb8d --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/ClickFriend.kt @@ -0,0 +1,78 @@ +/* + * 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.player + +import com.lambda.event.events.MouseEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.friend.FriendManager +import com.lambda.friend.FriendManager.befriend +import com.lambda.friend.FriendManager.isFriend +import com.lambda.friend.FriendManager.unfriend +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.Communication.info +import com.lambda.util.Mouse +import com.lambda.util.world.raycast.RayCastUtils.entityResult +import net.minecraft.client.network.OtherClientPlayerEntity +import org.lwjgl.glfw.GLFW.GLFW_MOD_ALT +import org.lwjgl.glfw.GLFW.GLFW_MOD_CONTROL +import org.lwjgl.glfw.GLFW.GLFW_MOD_SHIFT +import org.lwjgl.glfw.GLFW.GLFW_MOD_SUPER + +object ClickFriend : Module( + name = "ClickFriend", + description = "Add or remove friends with a single click", + defaultTags = setOf(ModuleTag.PLAYER) +) { + private val friendButton by setting("Friend Button", Mouse.Button.Middle, description = "Button to press to befriend a player") + private val friendAction by setting("Action", Mouse.Action.Release, description = "What mouse action should add or remove the player") + private val comboUnfriend by setting("Combo Unfriend", false, description = "Press a key and right click a player to unfriend") + private val modUnfriend by setting("Combo Key", MouseMod.Shift, description = "The key to press to activate the unfriend combo") { comboUnfriend } + + init { + listen { + if (it.button != friendButton || + it.action != friendAction || + mc.currentScreen != null + ) return@listen + + val target = mc.crosshairTarget?.entityResult?.entity as? OtherClientPlayerEntity + ?: return@listen + + if (modUnfriend.flagsPresent(it.modifiers) || !comboUnfriend) { + when { + target.isFriend && target.unfriend() -> { + this@ClickFriend.info(FriendManager.unfriendedText(target.name)) + } + !target.isFriend && target.befriend() -> { + this@ClickFriend.info(FriendManager.befriendedText(target.name)) + } + } + } + } + } + + private enum class MouseMod(val modifiers: Int) { + Shift(GLFW_MOD_SHIFT), + Control(GLFW_MOD_CONTROL), + Alt(GLFW_MOD_ALT), + Super(GLFW_MOD_SUPER); + + fun flagsPresent(flags: Int) = flags and modifiers == modifiers + } +} diff --git a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt index f2f4bbcf6..b611d7dfc 100644 --- a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt +++ b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt @@ -113,6 +113,19 @@ fun TextBuilder.literal(value: String) { styleAndAppend(Text.literal(value)) } +/** + * Adds a literal text. + * + * @param value The text. + * @see StyleBuilder for action + */ +@TextDsl +fun TextBuilder.literal(color: Color = Color.WHITE, value: String) { + color(color) { + literal(value) + } +} + /** * Adds a mutable key bind text. *