diff --git a/common/build.gradle.kts b/common/build.gradle.kts index a271651ef..664c4e5bb 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -27,6 +27,12 @@ dependencies { implementation(kotlin("reflect")) // Baritone - modImplementation("baritone-api:baritone-api:1.10.2") +// modImplementation("baritone-api:baritone-api:1.10.2") modImplementation("baritone-api:baritone-unoptimized-fabric:1.10.2") + + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() } diff --git a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 903b77141..3d2e07027 100644 --- a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -3,11 +3,18 @@ import com.lambda.Lambda; import com.lambda.event.EventFlow; import com.lambda.event.events.ClientEvent; +import com.lambda.event.events.ScreenEvent; +import com.lambda.event.events.ScreenHandlerEvent; import com.lambda.event.events.TickEvent; import com.lambda.module.modules.player.Interact; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.ScreenHandlerProvider; import net.minecraft.client.network.ClientPlayerInteractionManager; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; @@ -15,6 +22,8 @@ @Mixin(MinecraftClient.class) public class MinecraftClientMixin { + @Shadow @Nullable public Screen currentScreen; + @Inject(method = "tick", at = @At("HEAD")) void onTickPre(CallbackInfo ci) { EventFlow.post(new TickEvent.Pre()); @@ -38,6 +47,26 @@ private void onStartup(CallbackInfo ci) { EventFlow.post(new ClientEvent.Startup()); } + @Inject(method = "setScreen", at = @At("HEAD")) + private void onScreenOpen(@Nullable Screen screen, CallbackInfo ci) { + if (screen == null) return; + if (screen instanceof ScreenHandlerProvider handledScreen) { + EventFlow.post(new ScreenHandlerEvent.Open<>(handledScreen.getScreenHandler())); + } + + EventFlow.post(new ScreenEvent.Open<>(screen)); + } + + @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;removed()V", shift = At.Shift.AFTER)) + private void onScreenRemove(@Nullable Screen screen, CallbackInfo ci) { + if (currentScreen == null) return; + if (currentScreen instanceof ScreenHandlerProvider handledScreen) { + EventFlow.post(new ScreenHandlerEvent.Close<>(handledScreen.getScreenHandler())); + } + + EventFlow.post(new ScreenEvent.Close<>(currentScreen)); + } + @Redirect(method = "doItemUse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;isBreakingBlock()Z")) boolean injectMultiActon(ClientPlayerInteractionManager instance) { if (instance == null) return true; diff --git a/common/src/main/java/com/lambda/mixin/baritone/MixinBaritonePlayerContext.java b/common/src/main/java/com/lambda/mixin/baritone/MixinBaritonePlayerContext.java index 67c185cdf..eadb2657c 100644 --- a/common/src/main/java/com/lambda/mixin/baritone/MixinBaritonePlayerContext.java +++ b/common/src/main/java/com/lambda/mixin/baritone/MixinBaritonePlayerContext.java @@ -24,6 +24,8 @@ void syncRotationWithBaritone(CallbackInfoReturnable cir) { if (baritone != BaritoneUtils.getPrimary()) return; RotationManager rm = RotationManager.INSTANCE; - cir.setReturnValue(new Rotation((float) rm.getCurrentRotation().getYaw(), (float) rm.getCurrentRotation().getPitch())); + cir.setReturnValue(new Rotation( + (float) rm.getCurrentRotation().getYaw(), (float) rm.getCurrentRotation().getPitch()) + ); } } diff --git a/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java b/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java new file mode 100644 index 000000000..03da40258 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java @@ -0,0 +1,30 @@ +package com.lambda.mixin.entity; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.InteractionEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayerInteractionManager.class) +public class ClientPlayInteractionManagerMixin { + + @Final + @Shadow + private MinecraftClient client; + + @Inject(method = "interactBlock", at = @At("HEAD")) + public void interactBlockHead(final ClientPlayerEntity player, final Hand hand, final BlockHitResult hitResult, final CallbackInfoReturnable cir) { + if (client.world == null) return; + EventFlow.post(new InteractionEvent.Block(client.world, hitResult)); + } +} diff --git a/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java b/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java index 6f97b119c..9c6439c49 100644 --- a/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java @@ -1,5 +1,6 @@ package com.lambda.mixin.render; +import com.lambda.task.RootTask; import com.lambda.util.DebugInfoHud; import net.minecraft.client.gui.hud.DebugHud; import org.spongepowered.asm.mixin.Mixin; @@ -15,4 +16,9 @@ public class DebugHudMixin { private void onGetRightText(CallbackInfoReturnable> cir) { DebugInfoHud.addDebugInfo(cir.getReturnValue()); } + + @Inject(method = "getLeftText", at = @At(value = "TAIL")) + private void onGetLeftText(CallbackInfoReturnable> cir) { + RootTask.INSTANCE.addInfo(cir.getReturnValue()); + } } diff --git a/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java b/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java new file mode 100644 index 000000000..9f816cfbb --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java @@ -0,0 +1,21 @@ +package com.lambda.mixin.render; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.ScreenEvent; +import com.lambda.event.events.ScreenHandlerEvent; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(ScreenHandler.class) +public class ScreenHandlerMixin { + @Inject(method = "updateSlotStacks", at = @At("TAIL")) + private void onUpdateSlotStacksHead(int revision, List stacks, ItemStack cursorStack, CallbackInfo ci) { + EventFlow.post(new ScreenHandlerEvent.Loaded(revision, stacks, cursorStack)); + } +} 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 42f58d5d3..af12440bc 100644 --- a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java +++ b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java @@ -4,6 +4,7 @@ import com.lambda.event.events.WorldEvent; import net.minecraft.block.BlockState; import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; import net.minecraft.util.math.BlockPos; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -18,4 +19,9 @@ private void handleBlockUpdateInject(BlockPos pos, BlockState state, int flags, ci.cancel(); } } -} + + @Inject(method = "addEntity", at = @At("HEAD"), cancellable = true) + private void addEntity(Entity entity, CallbackInfo ci) { + if (EventFlow.post(new WorldEvent.EntitySpawn(entity)).isCanceled()) ci.cancel(); + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt b/common/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt new file mode 100644 index 000000000..b79603703 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/brigadier/argument/ItemArguments.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023 The Quilt Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Preserve binary compatibility when moving extensions between files + */ +@file:JvmMultifileClass +@file:JvmName("ArgumentsKt") + +package com.lambda.brigadier.argument + + +import com.lambda.brigadier.* +import com.lambda.brigadier.assumeSourceNotUsed +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.command.argument.ItemPredicateArgumentType +import net.minecraft.command.argument.ItemSlotArgumentType +import net.minecraft.command.argument.ItemStackArgument +import net.minecraft.command.argument.ItemStackArgumentType +import net.minecraft.item.ItemStack +import java.util.function.Predicate + +/** + * Reads the [ItemStack] predicate value from the + * argument in the receiver [ArgumentReader]. + * + * @see ItemPredicateArgumentType.getItemPredicate + */ +@JvmName("valueItemPredicateArg") +@BrigadierDsl +fun DefaultArgumentReader.value(): Predicate { + return ItemPredicateArgumentType.getItemStackPredicate(context.assumeSourceNotUsed(), name) +} + +/** + * Reads the integer value from the + * argument in the receiver [ArgumentReader]. + * + * @see ItemSlotArgumentType.getItemSlot + */ +@JvmName("valueItemSlotArg") +@BrigadierDsl +fun DefaultArgumentReader.value(): Int { + return ItemSlotArgumentType.getItemSlot(context.assumeSourceNotUsed(), name) +} + +/** + * Reads the [ItemStackArgument] value from the + * argument in the receiver [ArgumentReader]. + * + * @see ItemStackArgumentType.getItemStackArgument + */ +@JvmName("valueItemStackArg") +@BrigadierDsl +fun DefaultArgumentReader.value(): ItemStackArgument { + return ItemStackArgumentType.getItemStackArgument(context, name) +} + +/** + * Creates an item predicate argument with [name] as the parameter name. + * + * @param context The command build context + */ +@BrigadierDsl +fun itemPredicate( + name: String, + context: CommandRegistryAccess +): DefaultArgumentConstructor { + return argument(name, ItemPredicateArgumentType.itemPredicate(context)) +} + +/** + * Creates an item slot argument with [name] as the parameter name. + */ +@BrigadierDsl +fun itemSlot( + name: String +): DefaultArgumentConstructor { + return argument(name, ItemSlotArgumentType.itemSlot()) +} + +/** + * Creates an item stack argument with [name] as the parameter name. + * + * @param context The command build context + */ +@BrigadierDsl +fun itemStack( + name: String, + context: CommandRegistryAccess +): DefaultArgumentConstructor { + return argument(name, ItemStackArgumentType.itemStack(context)) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/command/LambdaCommand.kt b/common/src/main/kotlin/com/lambda/command/LambdaCommand.kt index 000aa4a74..1daabcd88 100644 --- a/common/src/main/kotlin/com/lambda/command/LambdaCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/LambdaCommand.kt @@ -4,7 +4,10 @@ import com.lambda.command.CommandManager.dispatcher import com.lambda.util.Nameable import com.lambda.util.primitives.extension.CommandBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder +import net.minecraft.command.CommandRegistryAccess import net.minecraft.command.CommandSource +import net.minecraft.registry.BuiltinRegistries +import net.minecraft.server.command.CommandManager abstract class LambdaCommand( final override val name: String, @@ -12,6 +15,10 @@ abstract class LambdaCommand( val usage: String = "", val description: String = "", ) : Nameable { + val registry: CommandRegistryAccess by lazy { + CommandManager.createRegistryAccess(BuiltinRegistries.createWrapperLookup()) + } + // ToDo: Include usage and description in the help command init { (listOf(name) + aliases).forEach { 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 cdf4d80a7..494e53c4d 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ConfigCommand.kt @@ -1,6 +1,5 @@ package com.lambda.command.commands -import com.lambda.brigadier.CommandResult.Companion.failure import com.lambda.brigadier.CommandResult.Companion.success import com.lambda.brigadier.argument.literal import com.lambda.brigadier.executeWithResult @@ -20,9 +19,8 @@ object ConfigCommand : LambdaCommand( required(literal("save")) { executeWithResult { Configuration.configurations.forEach { config -> - config.trySave()?.let { return@executeWithResult failure(it) } + config.trySave(true) } - this@ConfigCommand.info("Saved ${Configuration.configurations.size} configuration files.") return@executeWithResult success() } @@ -30,9 +28,8 @@ object ConfigCommand : LambdaCommand( required(literal("load")) { executeWithResult { Configuration.configurations.forEach { config -> - config.tryLoad()?.let { return@executeWithResult failure(it) } + config.tryLoad() } - this@ConfigCommand.info("Loaded ${Configuration.configurations.size} configuration files.") return@executeWithResult success() } diff --git a/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt new file mode 100644 index 000000000..f134fbb46 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt @@ -0,0 +1,108 @@ +package com.lambda.command.commands + +import com.lambda.brigadier.CommandResult.Companion.failure +import com.lambda.brigadier.CommandResult.Companion.success +import com.lambda.brigadier.argument.* +import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.required +import com.lambda.command.LambdaCommand +import com.lambda.interaction.material.ContainerManager +import com.lambda.interaction.material.ContainerManager.containerMatchSelection +import com.lambda.interaction.material.StackSelection.Companion.selectStack +import com.lambda.interaction.material.transfer.TransferResult +import com.lambda.util.Communication.info +import com.lambda.util.primitives.extension.CommandBuilder + +object TransferCommand : LambdaCommand( + name = "transfer", + usage = "transfer ", + description = "Transfer items from anywhere to anywhere", +) { + private var lastTransfer: TransferResult.Success? = null + + override fun CommandBuilder.create() { + required(itemStack("stack", registry)) { stack -> + required(integer("amount")) { amount -> + required(string("from")) { from -> + suggests { ctx, builder -> + val count = amount(ctx).value() + val selection = selectStack(count) { + isItem(stack(ctx).value().item) + } + containerMatchSelection(selection).forEach { + val available = it.available(selection) + val availableMsg = if (available == Int.MAX_VALUE) "∞" else available.toString() + builder.suggest("\"${it.name} with $availableMsg\"") + } + builder.buildFuture() + } + required(string("to")) { to -> + suggests { ctx, builder -> + val selection = selectStack(amount(ctx).value()) { + isItem(stack(ctx).value().item) + } + ContainerManager.container().forEach { + val space = it.spaceLeft(selection) + val spaceMsg = if (space == Int.MAX_VALUE) "∞" else space.toString() + if (space > 0) builder.suggest("\"${it.name} with $spaceMsg space left\"") + } + builder.buildFuture() + } + executeWithResult { + val selection = selectStack(amount().value()) { + isItem(stack().value().item) + } + val fromContainer = ContainerManager.container().find { + it.name == from().value().split(" with ").firstOrNull() + } ?: return@executeWithResult failure("From container not found") + + val toContainer = ContainerManager.container().find { + it.name == to().value().split(" with ").firstOrNull() + } ?: return@executeWithResult failure("To container not found") + + when (val result = fromContainer.transfer(selection, toContainer)) { + is TransferResult.Success -> { + info("$result started.") + lastTransfer = result + result.solve.onSuccess { _, _ -> + info("$lastTransfer completed.") + }.start(null) + return@executeWithResult success() + } + is TransferResult.MissingItems -> { + return@executeWithResult failure("Missing items: ${result.missing}") + } + is TransferResult.NoSpace -> { + return@executeWithResult failure("No space in ${toContainer.name}") + } + } + + return@executeWithResult success() + } + } + } + } + } + + required(literal("cancel")) { + executeWithResult { + lastTransfer?.solve?.cancel() ?: run { + return@executeWithResult failure("No transfer to cancel") + } + info("$lastTransfer cancelled") + lastTransfer = null + success() + } + } + + required(literal("undo")) { + executeWithResult { + lastTransfer?.undo ?: run { + return@executeWithResult failure("No transfer to undo") + } + info("Undoing $lastTransfer") + success() + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/Configuration.kt b/common/src/main/kotlin/com/lambda/config/Configuration.kt index 4e4e62641..a58e24769 100644 --- a/common/src/main/kotlin/com/lambda/config/Configuration.kt +++ b/common/src/main/kotlin/com/lambda/config/Configuration.kt @@ -8,7 +8,9 @@ import com.lambda.Lambda.gson import com.lambda.config.configurations.ModuleConfig import com.lambda.event.events.ClientEvent import com.lambda.event.listener.UnsafeListener.Companion.unsafeListener -import com.lambda.util.FolderRegister +import com.lambda.threading.runIO +import com.lambda.util.Communication.info +import com.lambda.util.Communication.logError import com.lambda.util.StringUtils.capitalize import java.io.File import java.time.Duration @@ -29,13 +31,13 @@ import kotlin.concurrent.fixedRateTimer * @property primary The primary file where the configuration is saved. * @property configurables A set of [Configurable] objects that this configuration manages. */ -abstract class Configuration( - val configName: String, -) : Jsonable { - private val primary = FolderRegister.config.resolve("$configName.json") - private val backup = File("${primary.parent}/${primary.nameWithoutExtension}-backup.${primary.extension}") +abstract class Configuration : Jsonable { + abstract val configName: String + abstract val primary: File val configurables = mutableSetOf() + private val backup: File + get() = File("${primary.parent}/${primary.nameWithoutExtension}-backup.${primary.extension}") init { unsafeListener { tryLoad() } @@ -93,37 +95,46 @@ abstract class Configuration( loadFromJson(JsonParser.parseReader(file.reader()).asJsonObject) } - fun tryLoad() = - runCatching { load(primary) } - .onSuccess { - val message = "${configName.capitalize()} config loaded." - LOG.info(message) - } - .onFailure { - runCatching { load(backup) } - .onSuccess { - LOG.info("${configName.capitalize()} config loaded from backup.") - } - .onFailure { backupError -> - LOG.error( - "Failed to load ${configName.capitalize()} config from backup, unrecoverable error.", - backupError - ) - } - } - .exceptionOrNull() + fun tryLoad() { + runIO { + runCatching { load(primary) } + .onSuccess { + val message = "${configName.capitalize()} config loaded." + LOG.info(message) + info(message) + } + .onFailure { + var message: String + runCatching { load(backup) } + .onSuccess { + message = "${configName.capitalize()} config loaded from backup" + LOG.info(message) + info(message) + } + .onFailure { error -> + message = + "Failed to load ${configName.capitalize()} config from backup, unrecoverable error" + LOG.error(message, error) + logError(message) + } + } + } + } - fun trySave() = - runCatching { save() } - .onSuccess { - val message = "Saved ${configName.capitalize()} config." - LOG.info(message) - } - .onFailure { - val message = "Failed to save ${configName.capitalize()} config" - LOG.error(message, it) - } - .exceptionOrNull() + fun trySave(logToChat: Boolean = false) { + runIO { + runCatching { save() } + .onSuccess { + val message = "Saved ${configName.capitalize()} config." + LOG.info(message) + if (logToChat) info(message) + } + .onFailure { + val message = "Failed to save ${configName.capitalize()} config" + logError(message) + } + } + } companion object { val configurations = mutableSetOf() diff --git a/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt b/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt deleted file mode 100644 index 85f1b810e..000000000 --- a/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.lambda.config - -import com.lambda.interaction.InteractionConfig -import com.lambda.util.world.raycast.RayCastMask - -class InteractionSettings( - c: Configurable, - vis: () -> Boolean = { true }, -) : InteractionConfig { - override val reach by c.setting("Reach", 5.0, 0.1..10.0, 0.1, "Players reach / range", "", vis) - override val resolution by c.setting("Resolution", 10, 1..100, 1, "Raycast resolution", "", vis) - override val rayCastMask by c.setting("Raycast Mask", RayCastMask.BOTH, "What to raycast against", vis) -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt b/common/src/main/kotlin/com/lambda/config/RotationSettings.kt deleted file mode 100644 index b5f34c005..000000000 --- a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.lambda.config - -import com.lambda.interaction.rotation.IRotationConfig -import com.lambda.interaction.rotation.RotationMode -import kotlin.math.max -import kotlin.math.min -import kotlin.random.Random - -class RotationSettings( - c: Configurable, - vis: () -> Boolean = { true }, -) : IRotationConfig { - override var rotationMode by c.setting("Mode", RotationMode.SYNC, "SILENT - server-side rotation, SYNC - server-side rotation; client-side movement, LOCK - Lock camera", vis) - override val keepTicks by c.setting("Keep Rotation", 3, 1..10, 1, "Ticks to keep rotation", "", vis) - override val resetTicks by c.setting("Reset Rotation", 3, 1..10, 1, "Ticks before rotation is reset", "", vis) - - var r1 by c.setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1, "Rotation Speed 1", "", vis) - var r2 by c.setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1, "Rotation Speed 2", "", vis) - - override val turnSpeed get() = Random.nextDouble(min(r1, r2), max(r1, r2) + 0.01) - - var speedMultiplier = 1.0 - - fun slowdownIf(flag: Boolean) { - speedMultiplier = (if (flag) 0.0 else 1.0) - .coerceIn( - speedMultiplier - 0.3, // slowdown faster - speedMultiplier + 0.15 // accelerate slower - ) - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt index ddc11f5c8..b860ce7f5 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt @@ -1,8 +1,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration +import com.lambda.util.FolderRegister -/** - * The [FriendConfig] object represents the configuration file for the friends. - */ -object FriendConfig : Configuration(configName = "friends") +object FriendConfig : Configuration() { + override val configName get() = "friends" + override val primary = FolderRegister.config.resolve("$configName.json") +} diff --git a/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt index 11391d85c..8165f3553 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt @@ -1,8 +1,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration +import com.lambda.util.FolderRegister -/** - * The [GuiConfig] object represents the configuration file for the GUI. - */ -object GuiConfig : Configuration(configName = "gui") +object GuiConfig : Configuration() { + override val configName get() = "gui" + override val primary = FolderRegister.config.resolve("$configName.json") +} diff --git a/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt index a1e7324e7..8d7690910 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt @@ -1,8 +1,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration +import com.lambda.util.FolderRegister -/** - * The [LambdaConfig] object represents the configuration file for the lambda. - */ -object LambdaConfig : Configuration(configName = "lambda") +object LambdaConfig : Configuration() { + override val configName get() = "lambda" + override val primary = FolderRegister.config.resolve("$configName.json") +} diff --git a/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt b/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt index e47128c49..ee1b6d611 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt @@ -1,11 +1,20 @@ package com.lambda.config.configurations import com.lambda.config.Configuration +import com.lambda.config.configurations.ModuleConfig.configName +import com.lambda.config.configurations.ModuleConfig.primary +import com.lambda.util.FolderRegister /** * The [ModuleConfig] object represents the configuration file for the [Module]s. * * This object is used to save and load the settings of all [Module]s in the system. + * + * @property configName The name of the configuration. + * @property primary The primary file where the configuration is saved. */ -object ModuleConfig : Configuration(configName = "modules") +object ModuleConfig : Configuration() { + override val configName get() = "modules" + override val primary = FolderRegister.config.resolve("$configName.json") +} diff --git a/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt new file mode 100644 index 000000000..3c86835ac --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt @@ -0,0 +1,14 @@ +package com.lambda.config.groups + +interface BuildConfig { + val breakCoolDown: Int + val placeCooldown: Int + val breakConfirmation: Boolean + val placeConfirmation: Boolean + val collectDrops: Boolean + val breakWeakBlocks: Boolean + val pathing: Boolean + val breaksPerTick: Int + val rotateForBreak: Boolean + val rotateForPlace: Boolean +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt new file mode 100644 index 000000000..eb900586f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -0,0 +1,27 @@ +package com.lambda.config.groups + +import com.lambda.config.Configurable + +class BuildSettings( + c: Configurable, + vis: () -> Boolean = { true } +) : BuildConfig { + enum class Page { + BREAK, PLACE, PATHING + } + + val page by c.setting("Build Page", Page.BREAK, "Current page", vis) + + override val breakCoolDown by c.setting("Break Cooldown", 0, 0..1000, 1, "Delay between breaking blocks", " ms") { vis() && page == Page.BREAK } + override val breakConfirmation by c.setting("Break Confirmation", false, "Wait for block break confirmation") { vis() && page == Page.BREAK } + override val breakWeakBlocks by c.setting("Break Weak Blocks", false, "Break blocks that dont have structural integrity (e.g: grass)") { vis() && page == Page.BREAK } + override val breaksPerTick by c.setting("Instant Breaks Per Tick", 10, 1..30, 1, "Maximum instant block breaks per tick") { vis() && page == Page.BREAK } + override val rotateForBreak by c.setting("Rotate For Break", false, "Rotate towards block while breaking") { vis() && page == Page.BREAK } + + override val placeCooldown by c.setting("Place Cooldown", 0, 0..1000, 1, "Delay between placing blocks", " ms") { vis() && page == Page.PLACE } + override val placeConfirmation by c.setting("Place Confirmation", false, "Wait for block placement confirmation") { vis() && page == Page.PLACE } + override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks") { vis() && page == Page.PLACE } + override val rotateForPlace by c.setting("Rotate For Place", true, "Rotate towards block while placing") { vis() && page == Page.PLACE } + + override val pathing by c.setting("Pathing", true, "Path to blocks") { vis() && page == Page.PATHING } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/groups/IRotationConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/IRotationConfig.kt new file mode 100644 index 000000000..47ff0a226 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/IRotationConfig.kt @@ -0,0 +1,45 @@ +package com.lambda.config.groups + +import com.lambda.interaction.rotation.RotationMode + +interface IRotationConfig { + /** + * - [RotationMode.NONE] No rotation. + * - [RotationMode.SILENT] Spoofing server-side rotation. + * - [RotationMode.SYNC] Spoofing server-side rotation and adjusting client-side movement based on reported rotation (for Grim). + * - [RotationMode.LOCK] Locks the camera client-side. + */ + val rotationMode: RotationMode + + /** + * The rotation speed (in degrees). + */ + val turnSpeed: Double + + /** + * Ticks the rotation should not be changed. + */ + val keepTicks: Int + + /** + * Ticks to rotate back to the actual rotation. + */ + val resetTicks: Int + + /** + * If true, rotation will be instant without any transition. If false, rotation will transition over time. + */ + val instant: Boolean + + /** + * The mean (average) value for the Gaussian distribution used to calculate rotation speed. + * This value represents the center of the distribution. + */ + val mean: Double + + /** + * The standard deviation for the Gaussian distribution used to calculate rotation speed. + * This value represents the spread or dispersion of the distribution. + */ + val derivation: Double +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/InteractionConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt similarity index 68% rename from common/src/main/kotlin/com/lambda/interaction/InteractionConfig.kt rename to common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt index 3eb58020f..919478485 100644 --- a/common/src/main/kotlin/com/lambda/interaction/InteractionConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt @@ -1,6 +1,4 @@ -package com.lambda.interaction - -import com.lambda.util.world.raycast.RayCastMask +package com.lambda.config.groups interface InteractionConfig { /** @@ -13,5 +11,6 @@ interface InteractionConfig { */ val resolution: Int - val rayCastMask: RayCastMask + val useRayCast: Boolean + val swingHand: Boolean } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt new file mode 100644 index 000000000..1ddb53ea6 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt @@ -0,0 +1,14 @@ +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.util.world.raycast.RayCastMask + +class InteractionSettings( + c: Configurable, + vis: () -> Boolean = { true }, +) : InteractionConfig { + override val reach by c.setting("Reach", 4.9, 0.1..10.0, 0.1, "Players reach / range", " blocks", vis) + override val useRayCast by c.setting("Raycast", false, "Verify hit vector with ray casting (for very strict ACs)", vis) + override val resolution by c.setting("Resolution", 5, 1..20, 1, "How many raycast checks per surface (will be squared)") { vis() && useRayCast } + override val swingHand by c.setting("Swing Hand", true, "Swing hand on interactions", vis) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt new file mode 100644 index 000000000..e464bca50 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt @@ -0,0 +1,33 @@ +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.interaction.rotation.RotationMode +import kotlin.math.* +import kotlin.random.Random + +class RotationSettings( + c: Configurable, + vis: () -> Boolean = { true }, +) : IRotationConfig { + override var rotationMode by c.setting("Mode", RotationMode.SYNC, "SILENT - server-side rotation, SYNC - server-side rotation; client-side movement, LOCK - Lock camera", vis) + override val keepTicks by c.setting("Keep Rotation", 3, 1..10, 1, "Ticks to keep rotation", " ticks", vis) + override val resetTicks by c.setting("Reset Rotation", 3, 1..10, 1, "Ticks before rotation is reset", " ticks", vis) + override var instant by c.setting("Instant Rotation", true, "Instantly rotate", vis) + override var mean by c.setting("Mean", 20.0, 1.0..80.0, 0.1, "Average rotation speed", unit = "°") { vis() && !instant } + override var derivation by c.setting("Standard Deviation", 5.0, 0.0..20.0, 0.1, "Spread of rotation speeds", unit = "°") { vis() && !instant } + + override val turnSpeed get() = abs(nextGaussian(mean, derivation)) + + var speedMultiplier = 1.0 + + private fun nextGaussian( + mean: Double = 0.0, + deviation: Double = 1.0 + ): Double { + val u1 = Random.nextDouble() + val u2 = Random.nextDouble() + + val randStdNormal = sqrt(-2.0 * ln(u1)) * cos(2.0 * PI * u2) + return mean + deviation * randStdNormal + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/core/Loader.kt b/common/src/main/kotlin/com/lambda/core/Loader.kt index 54a73eba7..b0d684eed 100644 --- a/common/src/main/kotlin/com/lambda/core/Loader.kt +++ b/common/src/main/kotlin/com/lambda/core/Loader.kt @@ -8,6 +8,7 @@ import com.lambda.graphics.renderer.gui.font.LambdaFont import com.lambda.gui.GuiConfigurable import com.lambda.interaction.PlayerPacketManager import com.lambda.interaction.RotationManager +import com.lambda.interaction.material.ContainerManager import com.lambda.module.ModuleRegistry import com.lambda.sound.SoundRegistry import com.lambda.util.Communication.ascii @@ -23,6 +24,7 @@ object Loader { GuiConfigurable, FriendRegistry, SoundRegistry, + ContainerManager ) fun initialize() { diff --git a/common/src/main/kotlin/com/lambda/event/EventFlow.kt b/common/src/main/kotlin/com/lambda/event/EventFlow.kt index 4037d57ec..f9f5f616b 100644 --- a/common/src/main/kotlin/com/lambda/event/EventFlow.kt +++ b/common/src/main/kotlin/com/lambda/event/EventFlow.kt @@ -1,16 +1,13 @@ package com.lambda.event +import com.lambda.context.SafeContext import com.lambda.event.callback.ICancellable import com.lambda.event.listener.Listener import com.lambda.threading.runConcurrent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import com.lambda.threading.runSafe +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.* /** @@ -31,7 +28,7 @@ object EventFlow { * useful when you have multiple independent [Job]s running in parallel. */ val lambdaScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private val concurrentFlow = MutableSharedFlow( + val concurrentFlow = MutableSharedFlow( extraBufferCapacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST ) @@ -52,6 +49,38 @@ object EventFlow { } } + suspend inline fun awaitEvent( + noinline predicate: SafeContext.(E) -> Boolean = { true }, + ) = concurrentFlow.filterIsInstance().first { + runSafe { + predicate(it) + } ?: false + } + + suspend inline fun awaitEventUnsafe( + noinline predicate: (E) -> Boolean = { true }, + ) = concurrentFlow.filterIsInstance().first(predicate) + + suspend inline fun awaitEvent( + timeout: Long, + noinline predicate: (E) -> Boolean = { true }, + ) = runBlocking { + withTimeout(timeout) { + concurrentFlow.filterIsInstance().first(predicate) + } + } + + suspend inline fun awaitEvents( + crossinline predicate: (E) -> Boolean = { true }, + ): Flow = flow { + concurrentFlow + .filterIsInstance() + .filter { predicate(it) } + .collect { + emit(it) + } + } + /** * Posts an [Event] to the event flow [concurrentFlow] and the synchronous [Listener]s. * diff --git a/common/src/main/kotlin/com/lambda/event/events/InteractionEvent.kt b/common/src/main/kotlin/com/lambda/event/events/InteractionEvent.kt new file mode 100644 index 000000000..8fce4adc7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/event/events/InteractionEvent.kt @@ -0,0 +1,12 @@ +package com.lambda.event.events + +import com.lambda.event.Event +import net.minecraft.client.world.ClientWorld +import net.minecraft.util.hit.BlockHitResult + +sealed class InteractionEvent : Event { + class Block( + val world: ClientWorld, + val blockHitResult: BlockHitResult + ) : InteractionEvent() +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/event/events/RotationEvent.kt b/common/src/main/kotlin/com/lambda/event/events/RotationEvent.kt index ff8637073..9d8d4363f 100644 --- a/common/src/main/kotlin/com/lambda/event/events/RotationEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/RotationEvent.kt @@ -3,17 +3,8 @@ package com.lambda.event.events import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable -import com.lambda.interaction.InteractionConfig import com.lambda.interaction.RotationManager -import com.lambda.interaction.rotation.IRotationConfig import com.lambda.interaction.rotation.RotationContext -import com.lambda.interaction.visibilty.VisibilityChecker.findRotation -import com.lambda.threading.runSafe -import com.lambda.util.world.raycast.RayCastUtils.blockResult -import com.lambda.util.world.raycast.RayCastUtils.entityResult -import net.minecraft.entity.Entity -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Direction abstract class RotationEvent : Event { /** @@ -30,39 +21,7 @@ abstract class RotationEvent : Event { init { // Always check if baritone wants to rotate as well RotationManager.BaritoneProcessor.baritoneContext?.let { context -> - this.context = RotationContext(context.rotation, context.config) - } - } - - fun lookAtEntity( - rotationConfig: IRotationConfig, - interactionConfig: InteractionConfig, - entity: Entity, - ) { - runSafe { - findRotation(rotationConfig, interactionConfig, listOf(entity.boundingBox)) { - entityResult?.entity == entity - }?.let { rotationContext -> - context = rotationContext - } - } - } - - fun lookAtBlock( - rotationConfig: IRotationConfig, - interactionConfig: InteractionConfig, - blockPos: BlockPos, - sides: Set = emptySet(), - ) { - runSafe { - val state = world.getBlockState(blockPos) - val voxelShape = state.getOutlineShape(world, blockPos) - val boundingBoxes = voxelShape.boundingBoxes.map { it.offset(blockPos) } - findRotation(rotationConfig, interactionConfig, boundingBoxes, sides) { - blockResult?.blockPos == blockPos && (blockResult?.side in sides || sides.isEmpty()) - }?.let { rotationContext -> - context = rotationContext - } + this.context = context } } } diff --git a/common/src/main/kotlin/com/lambda/event/events/ScreenEvent.kt b/common/src/main/kotlin/com/lambda/event/events/ScreenEvent.kt new file mode 100644 index 000000000..fea72d4f9 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/event/events/ScreenEvent.kt @@ -0,0 +1,12 @@ +package com.lambda.event.events + +import com.lambda.event.Event +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenHandler + +sealed class ScreenEvent : Event { + class Open(val screen: T) : ScreenEvent() + class Close(val screen: T) : ScreenEvent() +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt b/common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt new file mode 100644 index 000000000..6e6cf8352 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt @@ -0,0 +1,15 @@ +package com.lambda.event.events + +import com.lambda.event.Event +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenHandler + +sealed class ScreenHandlerEvent : Event { + class Open(val screenHandler: H) : ScreenHandlerEvent() + class Close(val screenHandler: H) : ScreenHandlerEvent() + data class Loaded( + val revision: Int, + val stacks: List, + val cursorStack: ItemStack, + ) : ScreenHandlerEvent() +} \ No newline at end of file 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 ab98b38a4..ffb715aed 100644 --- a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt @@ -5,11 +5,11 @@ import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable import net.minecraft.block.BlockState import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity import net.minecraft.util.math.BlockPos import net.minecraft.world.chunk.WorldChunk abstract class WorldEvent : Event { - abstract class ChunkEvent : WorldEvent() { abstract val world: ClientWorld abstract val chunk: WorldChunk @@ -18,14 +18,20 @@ abstract class WorldEvent : Event { override val world: ClientWorld, override val chunk: WorldChunk ) : ChunkEvent() + class Unload( override val world: ClientWorld, override val chunk: WorldChunk ) : ChunkEvent() } + class BlockUpdate( val pos: BlockPos, val state: BlockState, val flags: Int ) : WorldEvent(), ICancellable by Cancellable() + + class EntitySpawn( + val entity: Entity + ) : WorldEvent(), ICancellable by Cancellable() } \ No newline at end of file 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 d1fbc86b6..7f24f3430 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -126,7 +126,7 @@ class SafeListener( noinline function: SafeContext.(T) -> Unit, ): SafeListener { val listener = SafeListener(priority, this, alwaysListen) { event -> - function(event as T) + function(event as T) // ToDo: run function always on game thread } syncListeners.subscribe(listener) diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt index fd36b3faa..8793136ee 100644 --- a/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt +++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vao/VAO.kt @@ -19,7 +19,7 @@ import com.lambda.graphics.gl.VaoUtils.bufferData import com.lambda.graphics.gl.VaoUtils.unbindIndexBuffer import com.lambda.graphics.gl.VaoUtils.unbindVertexArray import com.lambda.graphics.gl.VaoUtils.unbindVertexBuffer -import com.lambda.threading.runOnGameThread +import com.lambda.threading.runGameScheduled import com.mojang.blaze3d.systems.RenderSystem.drawElements import org.lwjgl.opengl.GL30C.* import java.awt.Color @@ -49,7 +49,7 @@ class VAO( val stride = attribGroup.stride objectSize = stride * drawMode.indicesCount - runOnGameThread { + runGameScheduled { vertices = byteBuffer(objectSize * 256 * 4) verticesPointer = address(vertices) verticesPosition = verticesPointer @@ -197,7 +197,7 @@ class VAO( } protected fun finalize() { - runOnGameThread { + runGameScheduled { glDeleteBuffers(ibo) glDeleteBuffers(vbo) glDeleteVertexArrays(vao) diff --git a/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt b/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt index 8060296d6..11031eb63 100644 --- a/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt @@ -2,9 +2,9 @@ package com.lambda.interaction import baritone.utils.PlayerMovementInput import com.lambda.Lambda.mc -import com.lambda.config.RotationSettings -import com.lambda.context.SafeContext +import com.lambda.config.groups.RotationSettings import com.lambda.core.Loadable +import com.lambda.context.SafeContext import com.lambda.event.EventFlow.post import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listener @@ -15,7 +15,7 @@ import com.lambda.interaction.rotation.Rotation.Companion.slerp import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.rotation.RotationMode import com.lambda.module.modules.client.Baritone -import com.lambda.threading.runOnGameThread +import com.lambda.threading.runGameScheduled import com.lambda.threading.runSafe import com.lambda.util.math.MathUtils.lerp import com.lambda.util.math.MathUtils.toRadian @@ -50,7 +50,7 @@ object RotationManager : Loadable { val packet = event.packet if (packet !is PlayerPositionLookS2CPacket) return@listener - runOnGameThread { + runGameScheduled { reset(Rotation(packet.yaw, packet.pitch)) } } @@ -80,6 +80,10 @@ object RotationManager : Loadable { currentRotation = currentContext?.let { context -> val rotationTo = if (keepTicks >= 0) context.rotation else player.rotation + if (context.config.instant) { + return@let rotationTo + } + var speedMultiplier = (context.config as? RotationSettings)?.speedMultiplier ?: 1.0 if (keepTicks < 0) speedMultiplier = 1.0 diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt new file mode 100644 index 000000000..87aacb975 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt @@ -0,0 +1,48 @@ +package com.lambda.interaction.construction + +import com.lambda.context.SafeContext +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.util.BlockUtils.blockPos +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.primitives.extension.Structure +import net.minecraft.structure.StructureTemplate +import net.minecraft.util.math.BlockBox +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box + +abstract class Blueprint { + abstract val structure: Structure + + open fun isDone(ctx: SafeContext) = + structure.all { (pos, targetState) -> + with(ctx) { + targetState.matches(pos.blockState(world), pos, world) + } + } + + companion object { + fun Box.toStructure(targetState: TargetState): Structure = + BlockPos.stream(this) + .map { it.blockPos } + .toList() + .associateWith { targetState } + + fun BlockBox.toStructure(targetState: TargetState): Structure = + BlockPos.stream(this) + .map { it.blockPos } + .toList() + .associateWith { targetState } + + fun BlockPos.toStructure(targetState: TargetState): Structure = + setOf(this) + .associateWith { targetState } + +// fun Schematic.fromSchematic() = +// this.blockMap.map { it.key to TargetState.BlockState(it.value) }.toMap() + + fun StructureTemplate.toStructure(): Structure = + blockInfoLists + .flatMap { it.all } + .associate { it.pos to TargetState.State(it.state) } + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt new file mode 100644 index 000000000..029537309 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt @@ -0,0 +1,38 @@ +package com.lambda.interaction.construction + +import com.lambda.context.SafeContext +import com.lambda.util.primitives.extension.Structure +import net.minecraft.util.math.Vec3i + +data class DynamicBlueprint( + val init: SafeContext.(Structure) -> Structure = { emptyMap() }, + val update: SafeContext.(Structure) -> Structure = { it }, +) : Blueprint() { + fun update(ctx: SafeContext) { + structure = ctx.update(structure) + } + + fun create(ctx: SafeContext) { + structure = ctx.init(structure) + } + + override var structure: Structure = emptyMap() + private set + + companion object { + fun offset(offset: Vec3i): SafeContext.(Structure) -> Structure = { + it.map { (pos, state) -> + pos.add(offset) to state + }.toMap() + } + + fun blueprintOnTick( + init: SafeContext.(Structure) -> Structure = { emptyMap() }, + onTick: SafeContext.(Structure) -> Structure + ) = DynamicBlueprint(init = init, update = onTick) + + fun Structure.toBlueprint( + onTick: SafeContext.(Structure) -> Structure + ) = DynamicBlueprint(init = { emptyMap() }, update = onTick) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt new file mode 100644 index 000000000..35b4020da --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt @@ -0,0 +1,11 @@ +package com.lambda.interaction.construction + +import com.lambda.util.primitives.extension.Structure + +data class StaticBlueprint( + override val structure: Structure +) : Blueprint() { + companion object { + fun Structure.toBlueprint() = StaticBlueprint(this) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt b/common/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt new file mode 100644 index 000000000..862b070f4 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt @@ -0,0 +1,43 @@ +package com.lambda.interaction.construction.context + +import com.lambda.context.SafeContext +import com.lambda.interaction.rotation.RotationContext +import com.lambda.util.world.raycast.RayCastUtils.distanceTo +import net.minecraft.block.BlockState +import net.minecraft.util.Hand +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d + +data class BreakContext( + val pov: Vec3d, + val result: BlockHitResult, + val rotation: RotationContext, + override val checkedState: BlockState, + override var hand: Hand, + val instantBreak: Boolean, +) : BuildContext { + override val resultingPos: BlockPos + get() = result.blockPos + + override val distance: Double by lazy { + result.distanceTo(pov) + } + + fun exposedSides(ctx: SafeContext) = + Direction.entries.filter { + ctx.world.isAir(result.blockPos.offset(it)) + } + + override val expectedState = checkedState.fluidState.blockState + + override fun compareTo(other: ComparableContext): Int { + return when (other) { + is BreakContext -> compareBy { + it.distance + }.compare(this, other) + else -> 1 + } + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt b/common/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt new file mode 100644 index 000000000..83cd5f4fa --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt @@ -0,0 +1,13 @@ +package com.lambda.interaction.construction.context + +import net.minecraft.block.BlockState +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos + +interface BuildContext : ComparableContext { + val distance: Double + val expectedState: BlockState + val checkedState: BlockState + val hand: Hand + val resultingPos: BlockPos +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/context/ComparableContext.kt b/common/src/main/kotlin/com/lambda/interaction/construction/context/ComparableContext.kt new file mode 100644 index 000000000..7ab3c2182 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/ComparableContext.kt @@ -0,0 +1,7 @@ +package com.lambda.interaction.construction.context + +interface ComparableContext : Comparable { + override fun compareTo(other: ComparableContext): Int { + return 0 + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt b/common/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt new file mode 100644 index 000000000..89645c3b2 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt @@ -0,0 +1,45 @@ +package com.lambda.interaction.construction.context + +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.rotation.RotationContext +import com.lambda.util.BlockUtils +import net.minecraft.block.BlockState +import net.minecraft.util.Hand +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d + +data class PlaceContext( + val pov: Vec3d, + val result: BlockHitResult, + val rotation: RotationContext, + override val distance: Double, + override val expectedState: BlockState, + override val checkedState: BlockState, + val targetState: TargetState, + override val hand: Hand, + val sneak: Boolean, + val insideBlock: Boolean, +) : BuildContext { + override val resultingPos: BlockPos + get() = result.blockPos.offset(result.side) + + override fun compareTo(other: ComparableContext): Int { + return when (other) { + is PlaceContext -> compareBy { + BlockUtils.fluids.indexOf(it.checkedState.fluidState.fluid) + }.thenByDescending { + it.checkedState.fluidState.level + }.thenBy { + it.hand + }.thenBy { + it.sneak + }.thenBy { + it.distance + }.thenBy { + it.insideBlock + }.compare(this, other) + else -> 1 + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt new file mode 100644 index 000000000..c457cd560 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt @@ -0,0 +1,125 @@ +package com.lambda.interaction.construction.result + +import baritone.api.pathing.goals.GoalBlock +import baritone.api.pathing.goals.GoalInverted +import com.lambda.interaction.construction.context.BreakContext +import com.lambda.interaction.material.ContainerManager.findBestAvailableTool +import com.lambda.interaction.material.ContainerManager.transfer +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.StackSelection.Companion.selectStack +import com.lambda.interaction.material.container.MainHandContainer +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.task.Task.Companion.failTask +import com.lambda.task.tasks.BreakBlock.Companion.breakBlock +import com.lambda.task.tasks.GoalTask.Companion.moveToGoalUntil +import net.minecraft.block.BlockState +import net.minecraft.item.Item +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Direction + +sealed class BreakResult : BuildResult() { + + /** + * Represents a successful break. All checks have been passed. + * @param context The context of the break. + */ + data class Success( + override val blockPos: BlockPos, + val context: BreakContext + ) : Resolvable, BreakResult() { + override val rank = Rank.BREAK_SUCCESS + + override val resolve get() = breakBlock(context) + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is Success -> context.compareTo(other.context) + else -> super.compareTo(other) + } + } + } + + /** + * Represents a break configuration where the hit side is not exposed to air. + * @param blockPos The position of the block that is not exposed. + * @param side The side that is not exposed. + */ + data class NotExposed( + override val blockPos: BlockPos, + val side: Direction + ) : Resolvable, BreakResult() { + override val rank = Rank.BREAK_NOT_EXPOSED + + override val resolve get() = failTask("Block is not exposed to air.") + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is NotExposed -> blockPos.compareTo(other.blockPos) + else -> super.compareTo(other) + } + } + } + + /** + * The equipped item is not suitable for breaking blocks. + * @param blockState The block state that is being broken. + * @param badItem The item that is being used. + */ + data class ItemCantMine( + override val blockPos: BlockPos, + val blockState: BlockState, + val badItem: Item + ) : Resolvable, BreakResult() { + override val rank = Rank.BREAK_ITEM_CANT_MINE + override val resolve get() = findBestAvailableTool(blockState) + ?.select() + ?.transfer(MainHandContainer) + ?.solve ?: run { + selectStack { + isItem(badItem).not() + }.transfer(MainHandContainer)?.solve ?: failTask("No item found or space") + } + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is ItemCantMine -> badItem.name.string.compareTo(other.badItem.name.string) + else -> super.compareTo(other) + } + } + } + + /** + * The block is a liquid and first has to be submerged. + * @param blockPos The position of the block that is a liquid. + */ + data class Submerge( + override val blockPos: BlockPos, + val blockState: BlockState, + val submerge: Set + ) : BreakResult() { + override val rank = Rank.BREAK_SUBMERGE + } + + /** + * The block is blocked by another liquid block that first has to be submerged. + */ + data class BlockedByLiquid( + override val blockPos: BlockPos, + val blockState: BlockState + ) : BreakResult() { + override val rank = Rank.BREAK_IS_BLOCKED_BY_LIQUID + } + + /** + * The player is standing on the block. + */ + data class PlayerOnTop( + override val blockPos: BlockPos, + val blockState: BlockState + ) : Navigable, BreakResult() { + override val rank = Rank.BREAK_PLAYER_ON_TOP + + override val goal = GoalInverted(GoalBlock(blockPos)) + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt new file mode 100644 index 000000000..43e1d8e88 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt @@ -0,0 +1,200 @@ +package com.lambda.interaction.construction.result + +import baritone.api.pathing.goals.GoalBlock +import baritone.api.pathing.goals.GoalNear +import baritone.process.BuilderProcess.GoalPlace +import com.lambda.interaction.construction.context.BuildContext +import com.lambda.interaction.material.ContainerManager.transfer +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.container.MainHandContainer +import com.lambda.task.Task.Companion.failTask +import net.minecraft.block.BlockState +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d + +abstract class BuildResult : ComparableResult { + abstract val blockPos: BlockPos + + /** + * The build action is done. + */ + data class Done( + override val blockPos: BlockPos, + ) : BuildResult() { + override val rank = Rank.DONE + } + + /** + * The build action is ignored. + */ + data class Ignored( + override val blockPos: BlockPos, + ) : BuildResult() { + override val rank = Rank.IGNORED + } + + /** + * The chunk at the target is not loaded. + * @param blockPos The position of the block that is in an unloaded chunk. + */ + data class ChunkNotLoaded( + override val blockPos: BlockPos + ) : Navigable, BuildResult() { + override val rank = Rank.CHUNK_NOT_LOADED + + override val goal = GoalBlock(blockPos) + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is ChunkNotLoaded -> blockPos.compareTo(other.blockPos) + else -> super.compareTo(other) + } + } + } + + /** + * The player has no permission to interact with the block. (E.g.: Spectator mode) + * @param blockPos The position of the block that is restricted. + */ + data class Restricted( + override val blockPos: BlockPos + ) : BuildResult() { + override val rank = Rank.BREAK_RESTRICTED + } + + /** + * The block needs server permission to be broken. (Needs op) + * @param blockPos The position of the block that needs permission. + * @param blockState The state of the block that needs permission. + */ + data class NoPermission( + override val blockPos: BlockPos, + val blockState: BlockState + ) : BuildResult() { + override val rank get() = Rank.BREAK_NO_PERMISSION + } + + /** + * The break target is out of the world border or height limit. + * @param blockPos The position of the block that is out of the world. + */ + data class OutOfWorld( + override val blockPos: BlockPos + ) : BuildResult() { + override val rank = Rank.OUT_OF_WORLD + } + + /** + * The block is unbreakable. + * @param blockPos The position of the block that is unbreakable. + * @param blockState The state of the block that is unbreakable. + */ + data class Unbreakable( + override val blockPos: BlockPos, + val blockState: BlockState + ) : BuildResult() { + override val rank = Rank.UNBREAKABLE + } + + /** + * The checked configuration hits on a side not in the player direction. + * @param blockPos The position of the block that is not exposed. + * @param side The side that is not exposed. + */ + data class NotVisible( + override val blockPos: BlockPos, + val hitPos: BlockPos, + val side: Direction, + val distance: Double + ) : Navigable, BuildResult() { + override val rank = Rank.NOT_VISIBLE + + override val goal = GoalPlace(blockPos) + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is NotVisible -> distance.compareTo(other.distance) + else -> super.compareTo(other) + } + } + } + + /** + * Player has an inefficient tool equipped. + * @param neededItem The best tool for the block state. + */ + data class WrongItem( + override val blockPos: BlockPos, + val context: BuildContext, + val neededItem: Item + ) : Resolvable, BuildResult() { + override val rank = Rank.WRONG_ITEM + + override val resolve get() = + neededItem.select().transfer(MainHandContainer)?.solve ?: failTask("Item ${neededItem.name.string} not found") + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is WrongItem -> context.compareTo(other.context) + else -> super.compareTo(other) + } + } + } + + /** + * The Player has the wrong item stack selected. + * @param blockPos The position of the block that needs a different tool. + * @param neededStack The best tool for the block state. + */ + data class WrongStack( + override val blockPos: BlockPos, + val context: BuildContext, + val neededStack: ItemStack + ) : Resolvable, BuildResult() { + override val rank = Rank.WRONG_ITEM + + override val resolve get() = + neededStack.select().transfer(MainHandContainer)?.solve ?: failTask("Stack ${neededStack.name.string} not found") + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is WrongItem -> context.compareTo(other.context) + else -> super.compareTo(other) + } + } + } + + /** + * Represents a break out of reach. + * @param blockPos The position of the block that is out of reach. + * @param startVec The start vector of the reach. + * @param hitVec The hit vector of the reach. + * @param reach The maximum reach distance. + * @param side The side that is out of reach. + */ + data class OutOfReach( + override val blockPos: BlockPos, + val startVec: Vec3d, + val hitVec: Vec3d, + val reach: Double, + val side: Direction, + ) : Navigable, BuildResult() { + override val rank = Rank.OUT_OF_REACH + + val distance: Double by lazy { + startVec.distanceTo(hitVec) + } + + override val goal = GoalNear(blockPos, 2) + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is OutOfReach -> distance.compareTo(other.distance) + else -> super.compareTo(other) + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt new file mode 100644 index 000000000..3b02c3799 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt @@ -0,0 +1,9 @@ +package com.lambda.interaction.construction.result + +sealed interface ComparableResult> : Comparable> { + val rank: T + + override fun compareTo(other: ComparableResult): Int { + return rank.compareTo(other.rank) + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Navigable.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Navigable.kt new file mode 100644 index 000000000..0a075b2fd --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Navigable.kt @@ -0,0 +1,7 @@ +package com.lambda.interaction.construction.result + +import baritone.api.pathing.goals.Goal + +interface Navigable { + val goal: Goal +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt new file mode 100644 index 000000000..9847ba568 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt @@ -0,0 +1,124 @@ +package com.lambda.interaction.construction.result + +import baritone.api.pathing.goals.Goal +import baritone.api.pathing.goals.GoalBlock +import baritone.api.pathing.goals.GoalInverted +import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.task.Task +import com.lambda.task.tasks.BuildStructure.Companion.breakBlock +import com.lambda.task.tasks.GoalTask.Companion.moveToGoal +import com.lambda.task.tasks.GoalTask.Companion.moveToGoalUntil +import com.lambda.task.tasks.PlaceBlock.Companion.placeBlock +import net.minecraft.block.BlockState +import net.minecraft.item.ItemPlacementContext +import net.minecraft.item.ItemStack +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.shape.VoxelShape + +/** + * [PlaceResult] represents the result of a placement simulation. + * Holds data about the placement and the result of the simulation. + * Every [BuildResult] can [resolve] its own problem. + * Every [BuildResult] can be compared to another [BuildResult]. + * First based on the context, then based on the [Rank]. + */ +sealed class PlaceResult : BuildResult() { + /** + * Represents a successful placement. All checks have been passed. + * @param context The context of the placement. + */ + data class Success( + override val blockPos: BlockPos, + val context: PlaceContext, + ) : Resolvable, PlaceResult() { + override val rank = Rank.PLACE_SUCCESS + + override val resolve get() = placeBlock(context) + + override fun compareTo(other: ComparableResult): Int { + return when (other) { + is Success -> context.compareTo(other.context) + else -> super.compareTo(other) + } + } + } + + /** + * The calculated placement configuration does not match the simulated outcome. + * @param blockPos The position of the block that is not placed correctly. + * @param expected The expected placement configuration. + * @param simulated The simulated placement configuration. + */ + data class NoIntegrity( + override val blockPos: BlockPos, + val expected: BlockState, + val simulated: ItemPlacementContext + ) : PlaceResult() { + override val rank = Rank.PLACE_NO_INTEGRITY + } + + data class BlockedByPlayer( + override val blockPos: BlockPos + ) : Navigable, PlaceResult() { + override val rank = Rank.PLACE_BLOCKED_BY_PLAYER + + override val goal = GoalInverted(GoalBlock(blockPos)) + } + + /** + * The placement configuration cannot replace the block at the target position. + * @param simulated The simulated placement configuration. + */ + data class CantReplace( + override val blockPos: BlockPos, + val simulated: ItemPlacementContext + ) : Resolvable, PlaceResult() { + override val rank = Rank.PLACE_CANT_REPLACE + + override val resolve = breakBlock(blockPos) + } + + /** + * The placement configuration exceeds the maximum scaffold distance. + * @param simulated The simulated placement configuration. + */ + data class ScaffoldExceeded( + override val blockPos: BlockPos, + val simulated: ItemPlacementContext + ) : PlaceResult() { + override val rank = Rank.PLACE_SCAFFOLD_EXCEEDED + } + + /** + * The interaction with the block is restricted due to the feature being disabled. + */ + data class BlockFeatureDisabled( + override val blockPos: BlockPos, + val itemStack: ItemStack, + ) : PlaceResult() { + override val rank = Rank.PLACE_BLOCK_FEATURE_DISABLED + } + + /** + * The player has no permission to interact with the block. Or the stack cannot be used on the block. + */ + data class IllegalUsage( + override val blockPos: BlockPos + ) : PlaceResult() { + override val rank = Rank.PLACE_ILLEGAL_USAGE + } + + /** + * The item stack is not a block item. + * This will be resolved by analyzing interaction with non-block items. + */ + data class NotItemBlock( + override val blockPos: BlockPos, + val itemStack: ItemStack + ) : Resolvable, PlaceResult() { + override val rank = Rank.PLACE_NOT_ITEM_BLOCK + + override val resolve get() = TODO("Not expected") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt new file mode 100644 index 000000000..795d0d573 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt @@ -0,0 +1,34 @@ +package com.lambda.interaction.construction.result + +enum class Rank { + // solvable + BREAK_SUCCESS, + PLACE_SUCCESS, + WRONG_ITEM, + BREAK_ITEM_CANT_MINE, + PLACE_BLOCKED_BY_PLAYER, + NOT_VISIBLE, + OUT_OF_REACH, + BREAK_NOT_EXPOSED, + CHUNK_NOT_LOADED, + PLACE_CANT_REPLACE, + BREAK_PLAYER_ON_TOP, + PLACE_NOT_ITEM_BLOCK, + + // not solvable + OUT_OF_WORLD, + BREAK_RESTRICTED, + PLACE_NO_INTEGRITY, + BREAK_SUBMERGE, + BREAK_IS_BLOCKED_BY_LIQUID, + UNBREAKABLE, + BREAK_NO_PERMISSION, + PLACE_SCAFFOLD_EXCEEDED, + PLACE_BLOCK_FEATURE_DISABLED, + PLACE_ILLEGAL_USAGE, + + // not an issue + PLACE_IGNORED, + DONE, + IGNORED, +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt new file mode 100644 index 000000000..9bfb609c2 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt @@ -0,0 +1,7 @@ +package com.lambda.interaction.construction.result + +import com.lambda.task.Task + +interface Resolvable { + val resolve: Task<*> +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt new file mode 100644 index 000000000..50ffd268e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt @@ -0,0 +1,425 @@ +package com.lambda.interaction.construction.simulation + +import com.lambda.context.SafeContext +import com.lambda.interaction.RotationManager +import com.lambda.interaction.construction.Blueprint +import com.lambda.interaction.construction.context.BreakContext +import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.interaction.construction.result.BreakResult +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.PlaceResult +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.material.ContainerManager.findBestAvailableTool +import com.lambda.interaction.rotation.Rotation.Companion.rotationTo +import com.lambda.interaction.rotation.RotationContext +import com.lambda.interaction.visibilty.VisibilityChecker.mostCenter +import com.lambda.interaction.visibilty.VisibilityChecker.scanVisibleSurfaces +import com.lambda.module.modules.client.TaskFlow +import com.lambda.threading.runSafe +import com.lambda.util.BlockUtils +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.instantBreakable +import com.lambda.util.BlockUtils.vecOf +import com.lambda.util.item.ItemStackUtils.equal +import com.lambda.util.math.VecUtils.distSq +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.block.Block +import net.minecraft.block.OperatorBlock +import net.minecraft.block.pattern.CachedBlockPosition +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemPlacementContext +import net.minecraft.item.ItemUsageContext +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Hand +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import net.minecraft.util.shape.VoxelShape +import kotlin.math.pow +import kotlin.time.measureTime + +object BuildSimulator { + fun Blueprint.simulate(eye: Vec3d) = + runSafe { + structure.entries.flatMap { (pos, target) -> + checkRequirements(pos, target)?.let { + return@flatMap setOf(it) + } + checkPlaceResults(pos, target, eye).let { + if (it.isEmpty()) return@let + return@flatMap it + } + checkBreakResults(pos, eye).let { + if (it.isEmpty()) return@let + return@flatMap it + } + emptySet() + } + } ?: emptySet() + + private fun SafeContext.checkRequirements(pos: BlockPos, target: TargetState): BuildResult? { + /* the chunk is not loaded */ + if (!world.isChunkLoaded(pos)) { + return BuildResult.ChunkNotLoaded(pos) + } + + val state = pos.blockState(world) + + /* block is already in the correct state */ + if (target.matches(state, pos, world)) { + return BuildResult.Done(pos) + } + + /* block should be ignored */ + if (state.block in TaskFlow.ignoredBlocks) { + return BuildResult.Ignored(pos) + } + + /* the player is in the wrong game mode to alter the block state */ + if (player.isBlockBreakingRestricted(world, pos, interaction.currentGameMode)) { + return BuildResult.Restricted(pos) + } + + /* the player has no permissions to alter the block state */ + if (state.block is OperatorBlock && !player.isCreativeLevelTwoOp) { + return BuildResult.NoPermission(pos, state) + } + + /* block is outside the world so it cant be altered */ + if (!world.worldBorder.contains(pos) || world.isOutOfHeightLimit(pos)) { + return BuildResult.OutOfWorld(pos) + } + + /* block is unbreakable, so it cant be broken or replaced */ + if (state.getHardness(world, pos) < 0) { + return BuildResult.Unbreakable(pos, state) + } + + return null + } + + private fun SafeContext.checkPlaceResults( + pos: BlockPos, + target: TargetState, + eye: Vec3d + ): Set { + val acc = mutableSetOf() + + if (target is TargetState.Air || !pos.blockState(world).isReplaceable) return acc + + val interact = TaskFlow.interact + val rotation = TaskFlow.rotation + + Direction.entries.forEach { neighbor -> + val hitPos = pos.offset(neighbor) + val hitSide = neighbor.opposite + + val voxelShape = hitPos.blockState(world).getOutlineShape(world, hitPos) + if (voxelShape.isEmpty) return@forEach + + val boxes = voxelShape.boundingBoxes.map { it.offset(hitPos) } + + if (boxes.all { it.center.distanceTo(eye) > interact.reach + 1 }) { + acc.add(BuildResult.OutOfReach(pos, eye, hitPos.vecOf(hitSide), interact.reach, hitSide)) + return@forEach + } + + val verify: HitResult.() -> Boolean = { + blockResult?.blockPos == hitPos && blockResult?.side == hitSide + } + val validHits = mutableMapOf() + val reachSq = interact.reach.pow(2) + + boxes.forEach { box -> + val res = if (TaskFlow.interact.useRayCast) interact.resolution else 2 + scanVisibleSurfaces(eye, box, setOf(hitSide), res) { side, vec -> + if (eye distSq vec > reachSq) { +// acc.add(BuildResult.OutOfReach(pos, eye, vec, interact.reach, side)) + return@scanVisibleSurfaces + } + + validHits[vec] = if (TaskFlow.interact.useRayCast) { + val cast = eye.rotationTo(vec) + .rayCast(interact.reach, eye) ?: return@scanVisibleSurfaces + if (!cast.verify()) return@scanVisibleSurfaces + + cast + } else { + BlockHitResult( + vec, + side, + hitPos, + false + ) + } + + } + } + + if (validHits.isEmpty()) { + acc.add(BuildResult.NotVisible(pos, hitPos, hitSide, eye.distanceTo(hitPos.vecOf(hitSide)))) + return@forEach + } + + validHits.keys.mostCenter?.let { optimum -> + validHits.minByOrNull { optimum distSq it.key }?.let { closest -> + val optimumRotation = eye.rotationTo(closest.key) + RotationContext(optimumRotation, rotation, closest.value, verify) + } + }?.let { rotation -> + val optimalStack = target.getStack(world, pos) + + val usageContext = ItemUsageContext( + player, + Hand.MAIN_HAND, // ToDo: notice that the hand may have a different item stack and simulation will be wrong + rotation.hitResult?.blockResult, + ) + val cachePos = CachedBlockPosition( + usageContext.world, + usageContext.blockPos, + false + ) + val canBePlacedOn = optimalStack.canPlaceOn( + usageContext.world.registryManager.get(RegistryKeys.BLOCK), + cachePos, + ) + if (!player.abilities.allowModifyWorld && !canBePlacedOn) { + acc.add(PlaceResult.IllegalUsage(pos)) + return@forEach + } + + var context = ItemPlacementContext(usageContext) + + if (!optimalStack.item.isEnabled(world.enabledFeatures)) { + acc.add(PlaceResult.BlockFeatureDisabled(pos, optimalStack)) + return@forEach + } + + if (!context.canPlace()) { + acc.add(PlaceResult.CantReplace(pos, context)) + return@forEach + } + + val blockItem = optimalStack.item as? BlockItem ?: run { + acc.add(PlaceResult.NotItemBlock(pos, optimalStack)) + return@forEach + } + + val checked = blockItem.getPlacementContext(context) + if (checked == null) { + acc.add(PlaceResult.ScaffoldExceeded(pos, context)) + return@forEach + } else { + context = checked + } + + val resultState = blockItem.getPlacementState(context) ?: run { + acc.add(PlaceResult.CantReplace(pos, context)) + return@forEach + } + + if (!target.matches(resultState, pos, world)) { + acc.add(PlaceResult.NoIntegrity(pos, resultState, context)) + return@forEach + } + + val blockHit = rotation.hitResult?.blockResult ?: return@forEach + val hitBlock = blockHit.blockPos.blockState(world).block + val shouldSneak = hitBlock in BlockUtils.interactionBlacklist + + val placeContext = PlaceContext( + eye, + blockHit, + rotation, + eye.distanceTo(blockHit.pos), + resultState, + blockHit.blockPos.blockState(world), + target, + Hand.MAIN_HAND, + shouldSneak, + false + ) + + /* player is colliding with the place box */ + if (world.canCollide(player, Box(pos))) { + acc.add(PlaceResult.BlockedByPlayer(pos)) + return@forEach + } + + val currentHandStack = player.getStackInHand(Hand.MAIN_HAND) + if (target is TargetState.Stack && !target.itemStack.equal(currentHandStack)) { + acc.add(BuildResult.WrongStack(pos, placeContext, target.copy)) + return@forEach + } + + if (optimalStack.item != currentHandStack.item) { + acc.add(BuildResult.WrongItem(pos, placeContext, optimalStack.item)) + return@forEach + } + + acc.add(PlaceResult.Success(pos, placeContext)) + } + } + + return acc + } + + private fun SafeContext.checkBreakResults(pos: BlockPos, eye: Vec3d): Set { + val acc = mutableSetOf() + val state = pos.blockState(world) + + /* is a block that will be destroyed by breaking adjacent blocks */ + if (TaskFlow.build.breakWeakBlocks && state.block.hardness == 0f && !state.isAir) { + acc.add(BuildResult.Ignored(pos)) + return acc + } + + /* player is standing on top of the block */ + val pBox = player.boundingBox + val aabb = Box(pBox.minX, pBox.minY - 1.0E-6, pBox.minZ, pBox.maxX, pBox.minY, pBox.maxZ) + world.findSupportingBlockPos(player, aabb).orElse(null)?.let { support -> + if (support != pos) return@let + acc.add(BreakResult.PlayerOnTop(pos, state)) + return acc + } + + /* liquid needs to be submerged first to be broken */ + if (!state.fluidState.isEmpty && state.isReplaceable) { + val submerge = checkPlaceResults(pos, TargetState.Solid, eye) + acc.add(BreakResult.Submerge(pos, state, submerge)) + acc.addAll(submerge) + return acc + } + + val adjacentLiquids = Direction.entries.filter { + it != Direction.DOWN && !pos.offset(it).blockState(world).fluidState.isEmpty + } + + /* block has liquids next to it that will leak when broken */ + if (adjacentLiquids.isNotEmpty()) { + acc.add(BreakResult.BlockedByLiquid(pos, state)) + adjacentLiquids.forEach { + val submerge = checkPlaceResults(pos.offset(it), TargetState.Solid, eye) + acc.addAll(submerge) + } + return acc + } + + /* The current selected item cant mine the block */ + Hand.entries.forEach { + val stack = player.getStackInHand(it) + if (stack.isEmpty) return@forEach + if (stack.item.canMine(state, world, pos, player)) return@forEach + acc.add(BreakResult.ItemCantMine(pos, state, stack.item)) + return acc + } + + val interact = TaskFlow.interact + val rotation = TaskFlow.rotation + val currentRotation = RotationManager.currentRotation + val currentCast = currentRotation.rayCast(interact.reach, eye) + + val voxelShape = state.getOutlineShape(world, pos) + voxelShape.getClosestPointTo(eye).ifPresent { + // ToDo: Use closest point of shape + } + + val boxes = voxelShape.boundingBoxes.map { it.offset(pos) } + if (boxes.all { it.center.distanceTo(eye) > interact.reach + 1 }) { + acc.add(BuildResult.OutOfReach(pos, eye, pos.toCenterPos(), interact.reach, Direction.UP)) + return acc + } + + val verify: HitResult.() -> Boolean = { blockResult?.blockPos == pos } + /* the player is buried inside the block */ + if (boxes.any { it.contains(eye) }) { + currentCast?.blockResult?.let { blockHit -> + val rotationContext = RotationContext(currentRotation, rotation, currentCast, verify) + val breakContext = BreakContext( + eye, + blockHit, + rotationContext, + state, + player.activeHand, + instantBreakable(state, pos) + ) + acc.add(BreakResult.Success(pos, breakContext)) + return acc + } + } + + val validHits = mutableMapOf() + val reachSq = interact.reach.pow(2) + + boxes.forEach { box -> + val res = if (TaskFlow.interact.useRayCast) interact.resolution else 2 + scanVisibleSurfaces(eye, box, emptySet(), res) { side, vec -> + if (eye distSq vec > reachSq) { +// acc.add(BuildResult.OutOfReach(pos, eye, vec, interact.reach, side)) + return@scanVisibleSurfaces + } + + validHits[vec] = if (TaskFlow.interact.useRayCast) { + val cast = eye.rotationTo(vec) + .rayCast(interact.reach, eye) ?: return@scanVisibleSurfaces + if (!cast.verify()) return@scanVisibleSurfaces + + cast + } else { + BlockHitResult( + vec, + side, + pos, + false + ) + } + } + } + + validHits.keys.mostCenter?.let { optimum -> + validHits.minByOrNull { optimum distSq it.key }?.let { closest -> + val optimumRotation = eye.rotationTo(closest.key) + RotationContext(optimumRotation, rotation, closest.value, verify) + } + }?.let { bestRotation -> + val blockHit = bestRotation.hitResult?.blockResult ?: return@let + + val breakContext = BreakContext( + eye, + blockHit, + bestRotation, + state, + player.activeHand, + instantBreakable(state, pos) + ) + + /* player has a better tool for the job available */ + if (!player.isCreative) findBestAvailableTool(state)?.let { bestTool -> + Hand.entries.firstOrNull { + val stack = player.getStackInHand(it) + stack.item == bestTool + }?.let { hand -> + breakContext.hand = hand + acc.add(BreakResult.Success(pos, breakContext)) + return acc + } ?: run { + acc.add(BuildResult.WrongItem(pos, breakContext, bestTool)) + return acc + } + } + + acc.add(BreakResult.Success(pos, breakContext)) + } + + return acc + } + +// private fun SafeContext.playerFitsIn(BlockPos pos) { +// val pBox = player.boundingBox +// val aabb = Box(pBox.minX, pBox.minY - 1.0E-6, pBox.minZ, pBox.maxX, pBox.minY, pBox.maxZ) +// return world.findSupportingBlockPos(player, aabb).orElse(null) +// } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt b/common/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt new file mode 100644 index 000000000..ce05f9fe8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt @@ -0,0 +1,11 @@ +package com.lambda.interaction.construction.verify + +import net.minecraft.block.BlockState +import net.minecraft.client.world.ClientWorld +import net.minecraft.item.ItemStack +import net.minecraft.util.math.BlockPos + +interface StateMatcher { + fun matches(state: BlockState, pos: BlockPos, world: ClientWorld): Boolean + fun getStack(world: ClientWorld, pos: BlockPos): ItemStack +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt b/common/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt new file mode 100644 index 000000000..c07398559 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt @@ -0,0 +1,53 @@ +package com.lambda.interaction.construction.verify + +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.item.ItemUtils.block +import net.minecraft.block.BlockState +import net.minecraft.client.world.ClientWorld +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +sealed class TargetState : StateMatcher { + data object Air : TargetState() { + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state.isAir + override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = + ItemStack.EMPTY + } + data object Solid : TargetState() { + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state.isSolidBlock(world, pos) + override fun getStack(world: ClientWorld, pos: BlockPos) = + ItemStack(Items.NETHERRACK) // ToDo: Find any disposable block + } + data class Support(val direction: Direction) : TargetState() { + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + pos.offset(direction).blockState(world).isSolidBlock(world, pos.offset(direction)) + + override fun getStack(world: ClientWorld, pos: BlockPos) = + ItemStack(Items.NETHERRACK) // ToDo: Find any disposable block + } + data class State(val blockState: BlockState) : TargetState() { + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state == blockState + override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = + blockState.block.getPickStack(world, pos, blockState) + } + data class Block(val block: net.minecraft.block.Block) : TargetState() { + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state.block == block + override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = + block.getPickStack(world, pos, block.defaultState) + } + data class Stack(val itemStack: ItemStack) : TargetState() { + val copy: ItemStack = itemStack.copy() + + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state.block == copy.item.block + + override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = + copy + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/ContainerManager.kt b/common/src/main/kotlin/com/lambda/interaction/material/ContainerManager.kt new file mode 100644 index 000000000..ad4d35bbf --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/ContainerManager.kt @@ -0,0 +1,124 @@ +package com.lambda.interaction.material + +import com.lambda.core.Loadable +import com.lambda.event.events.InteractionEvent +import com.lambda.event.events.ScreenHandlerEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.container.* +import com.lambda.util.BlockUtils.blockEntity +import com.lambda.util.Communication.info +import com.lambda.util.item.ItemUtils +import com.lambda.util.primitives.extension.containerStacks +import net.minecraft.block.BlockState +import net.minecraft.block.entity.BlockEntity +import net.minecraft.block.entity.ChestBlockEntity +import net.minecraft.block.entity.EnderChestBlockEntity +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.ScreenHandlerType +import java.util.TreeSet + +// ToDo: Make this a Configurable to save container caches. Should use a cached region based storage system. +object ContainerManager : Loadable { + // ToDo: Maybe use reflection to get all containers? + private val container = TreeSet().apply { + add(CreativeContainer) + add(EnderChestContainer) + add(HotbarContainer) + add(InventoryContainer) + add(MainHandContainer) + add(OffHandContainer) + } + + private var lastInteractedBlockEntity: BlockEntity? = null + + init { + listener { + lastInteractedBlockEntity = it.blockHitResult.blockPos.blockEntity(world) + } + + listener> { event -> + // ToDo: ;-; i hate type erasure. + // The listener will be triggered for any H, not just GenericContainerScreenHandler + if (event.screenHandler !is GenericContainerScreenHandler) return@listener + + val handler = event.screenHandler + + when (val block = lastInteractedBlockEntity) { + is EnderChestBlockEntity -> { + if (handler.type != ScreenHandlerType.GENERIC_9X3) return@listener + + this@ContainerManager.info("Updating EnderChestContainer") + EnderChestContainer.update(handler.containerStacks) + } + is ChestBlockEntity -> { + // ToDo: Handle double chests and single chests + if (handler.type != ScreenHandlerType.GENERIC_9X6) return@listener + val stacks = handler.containerStacks + + this@ContainerManager.info("Updating ChestContainer") + container + .filterIsInstance() + .find { + it.blockPos == block.pos + }?.update(stacks) ?: container.add(ChestContainer(stacks, block.pos)) + } + } + lastInteractedBlockEntity = null + } + } + + fun container() = container.flatMap { + setOf(it) + it.shulkerContainer + }.sorted() + + fun StackSelection.transfer(destination: MaterialContainer) = + findContainerWithSelection(this)?.transfer(this, destination) + + fun findContainer( + block: (MaterialContainer) -> Boolean + ): MaterialContainer? = container().find(block) + + fun findContainerWithSelection( + selection: StackSelection + ): MaterialContainer? = + container().find { it.available(selection) >= selection.count } + + fun containerMatchSelection( + selection: StackSelection + ): Set = + container().filter { it.available(selection) >= selection.count }.toSet() + + fun findContainerWithSelection( + selectionBuilder: StackSelection.() -> Unit + ): MaterialContainer? { + val selection = StackSelection().apply(selectionBuilder) + return container().find { it.available(selection) >= selection.count } + } + + fun findContainerWithStacks( + count: Int = StackSelection.DEFAULT_AMOUNT, + selection: (ItemStack) -> Boolean, + ): MaterialContainer? = + findContainerWithSelection(selection.select()) + + fun findBestAvailableTool( + blockState: BlockState, + availableTools: Set = ItemUtils.tools, + ) = availableTools.map { + it to it.getMiningSpeedMultiplier(it.defaultStack, blockState) + }.filter { (item, speed) -> + speed > 1.0 + && item.isSuitableFor(blockState) + && findContainerWithSelection(item.select()) != null + }.maxByOrNull { + it.second + }?.first + +// fun SafeContext.nextDisposable() = player.combined.firstOrNull { it.item in TaskFlow.disposables } + + class NoContainerFound(selection: StackSelection): Exception("No container found matching $selection") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt new file mode 100644 index 000000000..f268250ec --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt @@ -0,0 +1,94 @@ +package com.lambda.interaction.material + +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.container.ShulkerBoxContainer +import com.lambda.interaction.material.transfer.TransferResult +import com.lambda.task.Task +import com.lambda.util.Nameable +import com.lambda.util.item.ItemStackUtils.count +import com.lambda.util.item.ItemStackUtils.empty +import com.lambda.util.item.ItemStackUtils.shulkerBoxContents +import com.lambda.util.item.ItemStackUtils.spaceLeft +import com.lambda.util.item.ItemUtils +import net.minecraft.item.ItemStack + +// ToDo: Make jsonable to persistently store them +abstract class MaterialContainer( + private val rank: Rank +) : Nameable, Comparable { + abstract var stacks: List + + val shulkerContainer get() = + stacks.filter { + it.item in ItemUtils.shulkerBoxes + }.map { stack -> + ShulkerBoxContainer( + stack.shulkerBoxContents, + containedIn = this@MaterialContainer, + shulkerStack = stack, + ) + }.toSet() + + fun update(stacks: List) { + this.stacks = stacks + } + + /** + * Withdraws items from the container to the player's inventory. + */ + @Task.Ta5kBuilder + abstract fun withdraw(selection: StackSelection): Task<*> + + /** + * Deposits items from the player's inventory into the container. + */ + @Task.Ta5kBuilder + abstract fun deposit(selection: StackSelection): Task<*> + + open fun matchingStacks(selection: StackSelection) = + selection.filterStacks(stacks) + + open fun matchingStacks(selection: (ItemStack) -> Boolean) = + matchingStacks(selection.select()) + + open fun available(selection: StackSelection) = + matchingStacks(selection).count + + open fun spaceLeft(selection: StackSelection) = + matchingStacks(selection).spaceLeft + stacks.empty * selection.stackSize + + fun transfer(selection: StackSelection, destination: MaterialContainer): TransferResult { + val amount = available(selection) + if (amount < selection.count) { + return TransferResult.MissingItems( selection.count - amount) + } + +// val space = destination.spaceLeft(selection) +// if (space == 0) { +// return TransferResult.NoSpace +// } +// +// val transferAmount = minOf(amount, space) +// selection.selector = { true } +// selection.count = transferAmount + + return TransferResult.Success(selection, from = this, to = destination) + } + + enum class Rank { + MAIN_HAND, + OFF_HAND, + HOTBAR, + INVENTORY, + CREATIVE, + SHULKER_BOX, + ENDER_CHEST, + CHEST, + STASH + } + + override fun compareTo(other: MaterialContainer) = + compareBy { + it.rank + }.compare(this, other) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt b/common/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt new file mode 100644 index 000000000..857bbbf71 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt @@ -0,0 +1,212 @@ +package com.lambda.interaction.material + +import com.lambda.util.BlockUtils.item +import com.lambda.util.item.ItemStackUtils.shulkerBoxContents +import net.minecraft.block.Block +import net.minecraft.enchantment.Enchantment +import net.minecraft.enchantment.EnchantmentHelper +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot +import kotlin.reflect.KClass + +/** + * [StackSelection] is a class that holds a predicate for matching [ItemStack]s. + */ +class StackSelection { + var selector: (ItemStack) -> Boolean = { true } + var count: Int = DEFAULT_AMOUNT + var inShulkerBox: Boolean = false + + var item: Item? = null + var itemClass: KClass? = null + private var damage: Int? = null + var itemStack: ItemStack? = null + val stackSize: Int + get() = optimalStack?.maxCount ?: 64 + + val optimalStack: ItemStack? + get() = itemStack ?: item?.let { ItemStack(it, count) } + + val filterStack: (ItemStack) -> Boolean + get() = { stack -> + if (inShulkerBox) { + stack.shulkerBoxContents.any { selector(it) } + } else { + selector(stack) + } + } + + val filterSlot: (Slot) -> Boolean + get() = { slot -> + filterStack(slot.stack) + } + + val filterStacks: (List) -> List + get() = { + it.filter(filterStack) + } + + val filterSlots: (List) -> List + get() = { slots -> + slots.filter { filterSlot(it) } + } + + /** + * returns a function that finds a shulker box to push matching items into. + */ + val findShulkerToPush = { stack: ItemStack -> + stack.shulkerBoxContents.let { inventory -> + if (inventory.all { selector(it) || it.isEmpty }) { + val storableItems = inventory.sumOf { +// if (it.isEmpty) item.itemStackLimit else it.maxStackSize - it.count + if (it.isEmpty) item?.maxCount ?: 0 else 0 + } + + if (storableItems > 0) storableItems else null + } else { + null + } + } + } + + /** + * returns a function that finds a shulker box to pull matching items from. + */ + val findShulkerToPull = { slot: Slot -> + slot.stack.shulkerBoxContents.let { inventory -> + val usableItems = inventory.sumOf { if (selector(it)) it.count else 0 } + + if (usableItems > 0) slot to usableItems else null + } + } + + /** + * [isItem] returns a predicate that matches a specific [Item]. + * @param item The [Item] to be matched. + * @return A predicate that matches the [Item]. + */ + fun isItem(item: Item): (ItemStack) -> Boolean { + this.item = item + return { it.item == item } + } + + /** + * [isItem] returns a predicate that matches a specific [Item] instance. + * @param T The instance of [Item] to be matched. + * @return A predicate that matches the [Item]. + */ + inline fun isItem(): (ItemStack) -> Boolean { + itemClass = T::class + return { it.item is T } + } + + /** + * [isBlock] returns a predicate that matches a specific [Block]. + * @param block The [Block] to be matched. + * @return A predicate that matches the [Block]. + */ + fun isBlock(block: Block): (ItemStack) -> Boolean { + item = block.item + return { it.item == block.item } + } + + /** + * [isItemStack] returns a predicate that matches a specific [ItemStack]. + * @param stack The [ItemStack] to be matched. + * @return A predicate that matches the [ItemStack]. + */ + fun isItemStack(stack: ItemStack): (ItemStack) -> Boolean { + this.itemStack = stack + return { ItemStack.areEqual(it, stack) } + } + + /** + * [hasDamage] returns a predicate that matches a specific damage value. + * @param damage The damage value to be matched. + * @return A predicate that matches the damage value. + */ + fun hasDamage(damage: Int): (ItemStack) -> Boolean { + this.damage = damage + return { it.damage == damage } + } + + /** + * [hasEnchantment] returns a predicate that matches a specific [Enchantment] and level. + * @param enchantment The [Enchantment] to be matched. + * @param level The level to be matched (if -1 will look for any level above 0). + * @return A predicate that matches the [Enchantment] and `level`. + */ + fun hasEnchantment(enchantment: Enchantment, level: Int = -1): (ItemStack) -> Boolean = { + if (level < 0) { + EnchantmentHelper.getLevel(enchantment, it) > 0 + } else { + EnchantmentHelper.getLevel(enchantment, it) == level + } + } + + /** + * Returns the negation of the original predicate. + * @return A new predicate that matches if the original predicate does not match. + */ + fun ((ItemStack) -> Boolean).not(): (ItemStack) -> Boolean { + return { !this(it) } + } + + /** + * Combines two predicates using the logical AND operator. + * @param otherPredicate The second predicate. + * @return A new predicate that matches if both inputs predicate match. + */ + infix fun ((ItemStack) -> Boolean).and(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean { + return { this(it) && otherPredicate(it) } + } + + /** + * Combines two predicates using the logical OR operator. + * @param otherPredicate The second predicate. + * @return A new predicate that matches if either input predicate matches. + */ + infix fun ((ItemStack) -> Boolean).or(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean { + return { this(it) || otherPredicate(it) } + } + + override fun toString() = buildString { + append("selection of ${count}x ") + item?.let { append(it.name.string) } + itemClass?.let { append(it.simpleName) } + itemStack?.let { append(it.name.string) } + damage?.let { append(" with damage $it") } + } + + companion object { + const val DEFAULT_AMOUNT = 1 + val FULL_SHULKERS: (ItemStack) -> Boolean = { stack -> + stack.shulkerBoxContents.none { it.isEmpty } + } + val EMPTY_SHULKERS: (ItemStack) -> Boolean = { stack -> + stack.shulkerBoxContents.all { it.isEmpty } + } + val EVERYTHING: (ItemStack) -> Boolean = { true } + + fun Item.select(): StackSelection = selectStack { isItem(this@select) } + fun ItemStack.select(): StackSelection = selectStack { isItemStack(this@select) } + fun ((ItemStack) -> Boolean).select() = selectStack { this@select } + + /** + * Builds a [StackSelection] with the given parameters. + * @param count The count of items to be selected. + * @param block The predicate to be used to select the items. + * @return A [StackSelection] with the given parameters. + */ + fun selectStack( + count: Int = DEFAULT_AMOUNT, + inShulkerBox: Boolean = false, + block: StackSelection.() -> (ItemStack) -> Boolean, + ) = StackSelection().apply { + selector = block() + this.count = count + this.inShulkerBox = inShulkerBox + } + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt new file mode 100644 index 000000000..222f589f5 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt @@ -0,0 +1,52 @@ +package com.lambda.interaction.material.container + +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.tasks.InventoryTask.Companion.deposit +import com.lambda.task.tasks.InventoryTask.Companion.withdraw +import com.lambda.task.tasks.OpenContainer.Companion.openContainer +import com.lambda.util.Communication.info +import net.minecraft.item.ItemStack +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.screen.ScreenHandler +import net.minecraft.util.math.BlockPos + +data class ChestContainer( + override var stacks: List, + val blockPos: BlockPos, + val containedInStash: StashContainer? = null, +) : MaterialContainer(Rank.CHEST) { + override val name = "Chest at ${blockPos.toShortString()}" + +// override fun prepare() = +// moveIntoEntityRange(blockPos).onSuccess { _, _ -> +//// when { +//// ChestBlock.hasBlockOnTop(world, blockPos) -> breakBlock(blockPos.up()) +//// ChestBlock.hasCatOnTop(world, blockPos) -> kill(cat) +//// } +// if (ChestBlock.isChestBlocked(world, blockPos)) { +// throw ChestBlockedException() +// } +// } + + override fun withdraw(selection: StackSelection) = + openContainer(blockPos) +// .withMaxAttempts(3) +// .withTimeout(20) + .onSuccess { open, screen -> + info("Withdrawing $selection from ${screen.type}") + withdraw(screen, selection).start(open) + } + + override fun deposit(selection: StackSelection) = + openContainer(blockPos) +// .withMaxAttempts(3) +// .withTimeout(20) + .onSuccess { open, screen -> + info("Depositing $selection to ${screen.type}") + deposit(screen, selection).start(open) + } + + class ChestBlockedException: Exception("The chest is blocked by another block or a cat") + class UnexpectedScreen(screenHandler: ScreenHandler): Exception("Unexpected screen. Got ${screenHandler.type}") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/CreativeContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/CreativeContainer.kt new file mode 100644 index 000000000..abf90f900 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/CreativeContainer.kt @@ -0,0 +1,54 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.mc +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task.Companion.buildTask +import com.lambda.util.item.ItemStackUtils.equal +import net.minecraft.item.ItemStack + +data object CreativeContainer : MaterialContainer(Rank.CREATIVE) { + override var stacks = emptyList() + override val name = "Creative" + + override fun available(selection: StackSelection): Int = + if (mc.player?.isCreative == true && selection.optimalStack != null) Int.MAX_VALUE else 0 + + override fun spaceLeft(selection: StackSelection) = Int.MAX_VALUE + + override fun deposit(selection: StackSelection) = buildTask("CreativeDeposit") { + if (!player.isCreative) { + // ToDo: Maybe switch gamemode? + throw NotInCreativeModeException() + } + + interaction.clickCreativeStack( + ItemStack.EMPTY, + 36 + player.inventory.selectedSlot + ) + } + + // Withdraws items from the creative menu to the player's main hand + override fun withdraw(selection: StackSelection) = buildTask("CreativeWithdraw") { + selection.optimalStack?.let { optimalStack -> + if (player.mainHandStack.equal(optimalStack)) return@buildTask + + if (!player.isCreative) { + // ToDo: Maybe switch gamemode? + throw NotInCreativeModeException() + } + + interaction.clickCreativeStack( + optimalStack, + 36 + player.inventory.selectedSlot + ) + return@buildTask + } + + throw NoOptimalStackException() + } + + class NotInCreativeModeException : IllegalStateException("Insufficient permission: not in creative mode") + class NoOptimalStackException : IllegalStateException("Cannot move item: no optimal stack") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt new file mode 100644 index 000000000..4dc768202 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt @@ -0,0 +1,38 @@ +package com.lambda.interaction.material.container + +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.Task.Companion.failTask +import com.lambda.task.tasks.OpenContainer.Companion.openContainer +import net.minecraft.item.ItemStack +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.util.math.BlockPos + +object EnderChestContainer : MaterialContainer(Rank.ENDER_CHEST) { + override var stacks = emptyList() + override val name = "EnderChest" + private var placePos: BlockPos? = null + +// override fun prepare(): Task<*> { +// TODO("Not yet implemented") +// } +// findBlock(Blocks.ENDER_CHEST).onSuccess { pos -> +// moveIntoEntityRange(pos) +// placePos = pos +// }.onFailure { +// acquireStack(Items.ENDER_CHEST.select()).onSuccess { _, stack -> +// placeContainer(stack).onSuccess { _, pos -> +// placePos = pos +// } +// } +// } + + override fun withdraw(selection: StackSelection): Task<*> { + TODO() + } + + override fun deposit(selection: StackSelection): Task<*> { + TODO() + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt new file mode 100644 index 000000000..c35b165c8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt @@ -0,0 +1,24 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.mc +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.task.tasks.InventoryTask.Companion.deposit +import com.lambda.util.player.SlotUtils.hotbar +import net.minecraft.item.ItemStack + +object HotbarContainer : MaterialContainer(Rank.HOTBAR) { + override var stacks: List + get() = mc.player?.hotbar ?: emptyList() + set(_) {} + override val name = "Hotbar" + + override fun withdraw(selection: StackSelection) = emptyTask("WithdrawFromHotbar") + + override fun deposit(selection: StackSelection): Task<*> { + val handler = mc.player?.currentScreenHandler ?: return emptyTask("NoScreenHandler") + return deposit(handler, selection) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt new file mode 100644 index 000000000..c47693fb8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt @@ -0,0 +1,20 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.mc +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.util.player.SlotUtils.combined +import net.minecraft.item.ItemStack + +object InventoryContainer : MaterialContainer(Rank.INVENTORY) { + override var stacks: List + get() = mc.player?.combined ?: emptyList() + set(_) {} + override val name = "Inventory" + + override fun withdraw(selection: StackSelection) = emptyTask("WithdrawFromInventory") + + override fun deposit(selection: StackSelection) = emptyTask("DepositToInventory") +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/MainHandContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/MainHandContainer.kt new file mode 100644 index 000000000..346ac9993 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/MainHandContainer.kt @@ -0,0 +1,48 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.mc +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task.Companion.buildTask +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.util.player.SlotUtils.combined +import com.lambda.util.player.SlotUtils.hotbar +import net.minecraft.item.ItemStack +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +object MainHandContainer : MaterialContainer(Rank.MAIN_HAND) { + override var stacks: List + get() = mc.player?.mainHandStack?.let { listOf(it) } ?: emptyList() + set(_) {} + override val name = "MainHand" + + override fun withdraw(selection: StackSelection) = emptyTask("WithdrawFromMainHand") + + override fun deposit(selection: StackSelection) = buildTask("DepositToMainHand") { + InventoryContainer.matchingStacks(selection).firstOrNull()?.let { stack -> + if (ItemStack.areEqual(stack, player.mainHandStack)) { + return@buildTask + } + + if (ItemStack.areEqual(stack, player.offHandStack)) { + connection.sendPacket( + PlayerActionC2SPacket( + PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND, + BlockPos.ORIGIN, + Direction.DOWN, + ), + ) + return@buildTask + } + + if (stack in player.hotbar) { + player.inventory.selectedSlot = player.hotbar.indexOf(stack) + return@buildTask + } + + interaction.pickFromInventory(player.combined.indexOf(stack)) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/OffHandContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/OffHandContainer.kt new file mode 100644 index 000000000..95d42af88 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/OffHandContainer.kt @@ -0,0 +1,46 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.mc +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task.Companion.buildTask +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.util.player.SlotUtils.combined +import com.lambda.util.player.SlotUtils.hotbar +import net.minecraft.item.ItemStack +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +object OffHandContainer : MaterialContainer(Rank.OFF_HAND) { + override var stacks: List + get() = mc.player?.offHandStack?.let { listOf(it) } ?: emptyList() + set(_) {} + override val name = "OffHand" + + override fun withdraw(selection: StackSelection) = emptyTask("WithdrawFromOffHand") + + override fun deposit(selection: StackSelection) = buildTask("DepositToOffHand") { + InventoryContainer.matchingStacks(selection).firstOrNull()?.let { stack -> + if (ItemStack.areEqual(stack, player.offHandStack)) { + return@buildTask + } + + if (stack in player.hotbar) { + player.inventory.selectedSlot = player.hotbar.indexOf(stack) + } else { + interaction.pickFromInventory(player.combined.indexOf(stack)) + } + + if (ItemStack.areEqual(stack, player.mainHandStack)) { + connection.sendPacket( + PlayerActionC2SPacket( + PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND, + BlockPos.ORIGIN, + Direction.DOWN, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/ShulkerBoxContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/ShulkerBoxContainer.kt new file mode 100644 index 000000000..53ad4d1dc --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/ShulkerBoxContainer.kt @@ -0,0 +1,63 @@ +package com.lambda.interaction.material.container + +import com.lambda.Lambda.LOG +import com.lambda.context.SafeContext +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.tasks.BuildStructure.Companion.breakAndCollectBlock +import com.lambda.task.tasks.InventoryTask.Companion.deposit +import com.lambda.task.tasks.InventoryTask.Companion.withdraw +import com.lambda.task.tasks.OpenContainer.Companion.openContainer +import com.lambda.task.tasks.PlaceContainer.Companion.placeContainer +import net.minecraft.item.ItemStack +import net.minecraft.screen.ShulkerBoxScreenHandler + +data class ShulkerBoxContainer( + override var stacks: List, + val containedIn: MaterialContainer, + val shulkerStack: ItemStack, +) : MaterialContainer(Rank.SHULKER_BOX) { + override val name = "${shulkerStack.name.string} in slot $slotInContainer in ${containedIn.name}" + + private val slotInContainer: Int get() = containedIn.stacks.indexOf(shulkerStack) + + class Withdraw( + private val selection: StackSelection, + private val shulkerStack: ItemStack + ) : Task() { + override fun SafeContext.onStart() { + placeContainer(shulkerStack).thenRun { _, placePos -> + openContainer(placePos).thenRun { _, screen -> + LOG.info("Opened shulker box screen now withdrawing $selection.") + withdraw(screen, selection).thenRun { _, _ -> + breakAndCollectBlock(placePos).onSuccess { _, _ -> + success(Unit) + } + } + } + }.start(this@Withdraw) + } + } + + override fun withdraw(selection: StackSelection) = Withdraw(selection, shulkerStack) + + class Deposit( + private val selection: StackSelection, + private val shulkerStack: ItemStack + ) : Task() { + override fun SafeContext.onStart() { + placeContainer(shulkerStack).thenRun { _, placePos -> + openContainer(placePos).thenRun { _, screen -> + deposit(screen, selection).thenRun { _, _ -> + breakAndCollectBlock(placePos).onSuccess { _, _ -> + success(Unit) + } + } + } + }.start(this@Deposit) + } + } + + override fun deposit(selection: StackSelection) = Deposit(selection, shulkerStack) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt new file mode 100644 index 000000000..463a5e10c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt @@ -0,0 +1,31 @@ +package com.lambda.interaction.material.container + +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.util.math.VecUtils.blockPos +import net.minecraft.item.ItemStack +import net.minecraft.util.math.Box + +data class StashContainer( + val chests: Set, + val pos: Box, +) : MaterialContainer(Rank.STASH) { + override var stacks: List + get() = chests.flatMap { it.stacks } + set(_) {} + override val name = "Stash at ${pos.center.blockPos.toShortString()}" + + override fun withdraw(selection: StackSelection): Task<*> { + TODO("Not yet implemented") + } + + override fun deposit(selection: StackSelection): Task<*> { + TODO("Not yet implemented") + } + + override fun available(selection: StackSelection): Int = + chests.sumOf { + it.available(selection) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferResult.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferResult.kt new file mode 100644 index 000000000..23bd5a5a8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferResult.kt @@ -0,0 +1,32 @@ +package com.lambda.interaction.material.transfer + +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.Task.Companion.failTask +import com.lambda.task.tasks.ContainerTransfer + +abstract class TransferResult { + abstract val solve: Task<*> + + data class Success( + val selection: StackSelection, + val from: MaterialContainer, + val to: MaterialContainer + ) : TransferResult() { + override val solve = ContainerTransfer(selection, from, to) + val undo = ContainerTransfer(selection, to, from) + + override fun toString() = "Transfer of [$selection] from [${from.name}] to [${to.name}]" + } + + data object NoSpace : TransferResult() { + // ToDo: Needs inventory space resolver. compressing or disposing + override val solve = failTask("NoSpace") + } + + data class MissingItems(val missing: Int) : TransferResult() { + // ToDo: Find other satisfying permutations + override val solve = failTask("MissingItems") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt new file mode 100644 index 000000000..b15e85586 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt @@ -0,0 +1,11 @@ +package com.lambda.interaction.material.transfer + +import com.lambda.task.Task + +abstract class TransferStep : Task() { + + + companion object { + + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/processing/ProcessingStep.kt b/common/src/main/kotlin/com/lambda/interaction/processing/ProcessingStep.kt new file mode 100644 index 000000000..17b5c7ff4 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/processing/ProcessingStep.kt @@ -0,0 +1,6 @@ +package com.lambda.interaction.processing + +import com.lambda.task.Task + +abstract class ProcessingStep : Task() { +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/processing/TaskPlanner.kt b/common/src/main/kotlin/com/lambda/interaction/processing/TaskPlanner.kt new file mode 100644 index 000000000..53d1a687a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/processing/TaskPlanner.kt @@ -0,0 +1,4 @@ +package com.lambda.interaction.processing + +object TaskPlanner { +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/rotation/IRotationConfig.kt b/common/src/main/kotlin/com/lambda/interaction/rotation/IRotationConfig.kt deleted file mode 100644 index c3593654d..000000000 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/IRotationConfig.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.lambda.interaction.rotation - -interface IRotationConfig { - /** - * - [RotationMode.SILENT] Spoofing server-side rotation. - * - [RotationMode.SYNC] Spoofing server-side rotation and adjusting client-side movement based on reported rotation (for Grim). - * - [RotationMode.LOCK] Locks the camera client-side. - */ - val rotationMode: RotationMode - - /** - * The rotation speed (in degrees). - */ - val turnSpeed: Double - - /** - * Ticks the rotation should not be changed. - */ - val keepTicks: Int - - /** - * Ticks to rotate back to the actual rotation. - */ - val resetTicks: Int -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt b/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt index d56c91f02..b462e22cb 100644 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt +++ b/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt @@ -35,9 +35,9 @@ data class Rotation(val yaw: Double, val pitch: Double) { fun rayCast( reach: Double, - mask: RayCastMask = RayCastMask.BOTH, eye: Vec3d? = null, fluids: Boolean = false, + mask: RayCastMask = RayCastMask.BOTH, ) = runSafe { rayCast(eye ?: player.eyePos, vector, reach, mask, fluids) } diff --git a/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt b/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt index d615c43c4..0d55a028a 100644 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt +++ b/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt @@ -1,12 +1,13 @@ package com.lambda.interaction.rotation -import com.lambda.interaction.RotationManager.currentRotation -import com.lambda.interaction.RotationManager.prevRotation -import com.lambda.interaction.rotation.Rotation.Companion.fixSensitivity +import com.lambda.config.groups.IRotationConfig +import net.minecraft.util.hit.HitResult data class RotationContext( val rotation: Rotation, val config: IRotationConfig, + val hitResult: HitResult? = null, + val verify: HitResult.() -> Boolean = { true }, ) { - val isPending: Boolean get() = rotation.fixSensitivity(prevRotation) == currentRotation + val isValid: Boolean get() = hitResult?.verify() == true } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/rotation/RotationMode.kt b/common/src/main/kotlin/com/lambda/interaction/rotation/RotationMode.kt index 9fb5a7900..c5aa4a501 100644 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/RotationMode.kt +++ b/common/src/main/kotlin/com/lambda/interaction/rotation/RotationMode.kt @@ -1,11 +1,13 @@ package com.lambda.interaction.rotation /** + * @property NONE No rotation. * @property SILENT Spoofing server-side rotation. * @property SYNC Spoofing server-side rotation and adjusting client-side movement based on reported rotation (for Grim). * @property LOCK Locks the camera client-side. */ enum class RotationMode { + NONE, SILENT, SYNC, LOCK diff --git a/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt b/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt index 1eec41c3d..4ea3dbe99 100644 --- a/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt +++ b/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt @@ -1,14 +1,20 @@ package com.lambda.interaction.visibilty import com.lambda.context.SafeContext -import com.lambda.interaction.InteractionConfig +import com.lambda.config.groups.InteractionConfig import com.lambda.interaction.RotationManager -import com.lambda.interaction.rotation.IRotationConfig +import com.lambda.config.groups.IRotationConfig import com.lambda.interaction.rotation.Rotation.Companion.rotationTo import com.lambda.interaction.rotation.RotationContext +import com.lambda.module.modules.client.TaskFlow +import com.lambda.util.BlockUtils.blockState import com.lambda.util.math.VecUtils.distSq import com.lambda.util.primitives.extension.component6 +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import com.lambda.util.world.raycast.RayCastUtils.entityResult +import net.minecraft.entity.Entity import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box import net.minecraft.util.math.Direction import net.minecraft.util.math.Vec3d @@ -16,77 +22,81 @@ import java.util.* import kotlin.math.pow object VisibilityChecker { - fun SafeContext.findRotation( + fun SafeContext.lookAtEntity( rotationConfig: IRotationConfig, - interact: InteractionConfig, + interactionConfig: InteractionConfig, + entity: Entity + ) = findRotation(listOf(entity.boundingBox), rotationConfig, interactionConfig) { + entityResult?.entity == entity + } + + fun SafeContext.lookAtBlock( + blockPos: BlockPos, + rotationConfig: IRotationConfig = TaskFlow.rotation, + interactionConfig: InteractionConfig = TaskFlow.interact, + sides: Set = emptySet() + ): RotationContext? { + val state = blockPos.blockState(world) + val voxelShape = state.getOutlineShape(world, blockPos) + val boundingBoxes = voxelShape.boundingBoxes.map { it.offset(blockPos) } + return findRotation(boundingBoxes, rotationConfig, interactionConfig, sides) { + blockResult?.blockPos == blockPos && (blockResult?.side in sides || sides.isEmpty()) + } + } + + fun SafeContext.findRotation( boxes: List, + rotationConfig: IRotationConfig = TaskFlow.rotation, + interact: InteractionConfig = TaskFlow.interact, sides: Set = emptySet(), verify: HitResult.() -> Boolean, ): RotationContext? { val eye = player.getCameraPosVec(mc.tickDelta) + val currentRotation = RotationManager.currentRotation + val currentCast = currentRotation.rayCast(interact.reach, eye) + if (boxes.any { it.contains(eye) }) { - return stay(rotationConfig) + return RotationContext(currentRotation, rotationConfig, currentCast, verify) } -// val currentRotation = RotationManager.currentRotation -// val currentCast = currentRotation.rayCast( -// interact.reach, -// interact.rayCastMask, -// eye -// ) -// val passed = currentCast?.let { it.verify() } ?: false -// // Slowdown or freeze if looking correct -// (rotationConfig as? RotationSettings)?.slowdownIf(passed) ?: run { -// if (passed) { -// return stay(rotationConfig) -// } -// } - - val validHits = mutableSetOf() + val validHits = mutableMapOf() + val reachSq = interact.reach.pow(2) boxes.forEach { box -> - scanVisibleSurfaces(box, sides, interact.resolution) { vec -> - if (eye distSq vec > interact.reach.pow(2)) return@scanVisibleSurfaces + scanVisibleSurfaces(player.eyePos, box, sides, interact.resolution) { _, vec -> + if (eye distSq vec > reachSq) return@scanVisibleSurfaces val newRotation = eye.rotationTo(vec) - val cast = newRotation.rayCast( - interact.reach, - interact.rayCastMask, - eye - ) ?: return@scanVisibleSurfaces + val cast = newRotation.rayCast(interact.reach, eye) ?: return@scanVisibleSurfaces if (!cast.verify()) return@scanVisibleSurfaces - validHits.add(vec) + validHits[vec] = cast } } - validHits.mostCenter?.let { optimum -> - validHits.minByOrNull { optimum distSq it }?.let { closest -> - val optimumRotation = eye.rotationTo(closest) - return RotationContext(optimumRotation, rotationConfig) + validHits.keys.mostCenter?.let { optimum -> + validHits.minByOrNull { optimum distSq it.key }?.let { closest -> + val optimumRotation = eye.rotationTo(closest.key) + return RotationContext(optimumRotation, rotationConfig, closest.value, verify) } } return null } - private fun stay(config: IRotationConfig) = - RotationContext(RotationManager.currentRotation, config) - - private inline fun SafeContext.scanVisibleSurfaces( + inline fun scanVisibleSurfaces( + eyes: Vec3d, box: Box, sides: Set, resolution: Int, - check: (Vec3d) -> Unit, + check: (Direction, Vec3d) -> Unit, ) { val shrunk = box.expand(-0.005) - box.getVisibleSurfaces(player.eyePos) + box.getVisibleSurfaces(eyes) .forEach { side -> - if (sides.isNotEmpty() && side !in sides) { - return@forEach - } + if (sides.isNotEmpty() && side !in sides) return@forEach val (minX, minY, minZ, maxX, maxY, maxZ) = shrunk.bounds(side) val stepX = (maxX - minX) / resolution val stepY = (maxY - minY) / resolution @@ -96,18 +106,18 @@ object VisibilityChecker { (0..resolution).forEach { j -> val y = if (stepY != 0.0) minY + stepY * j else minY val z = if (stepZ != 0.0) minZ + stepZ * ((if (stepX != 0.0) j else i)) else minZ - check(Vec3d(x, y, z)) + check(side, Vec3d(x, y, z)) } } } } - private val Set.mostCenter: Vec3d? + val Set.mostCenter: Vec3d? get() = reduceOrNull { acc, vec3d -> acc.add(vec3d) }?.multiply(1.0 / size.toDouble()) - private fun Box.bounds(side: Direction) = + fun Box.bounds(side: Direction) = when (side) { Direction.DOWN -> doubleArrayOf(minX, minY, minZ, maxX, minY, maxZ) Direction.UP -> doubleArrayOf(minX, maxY, minZ, maxX, maxY, maxZ) diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt index 9b76bc570..c77ab47ee 100644 --- a/common/src/main/kotlin/com/lambda/module/Module.kt +++ b/common/src/main/kotlin/com/lambda/module/Module.kt @@ -15,6 +15,7 @@ import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.event.listener.UnsafeListener import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.module.tag.ModuleTag +import com.lambda.task.Task import com.lambda.util.KeyCode import com.lambda.util.Nameable @@ -25,7 +26,7 @@ import com.lambda.util.Nameable * * Each [Module] has a [name], which is displayed in-game. * The [description] of the module is shown when hovering over - * the [ModuleButton] in the GUI and in [Command]s. + * the [ModuleButton] in the GUI and in [Commands]s. * The [Module] can be associated with a [Set] of [ModuleTag]s to allow for * easier filtering and searching in the GUI. * diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Baritone.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Baritone.kt index 892478d99..4a8393d0a 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Baritone.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Baritone.kt @@ -1,23 +1,13 @@ package com.lambda.module.modules.client -import com.lambda.interaction.rotation.IRotationConfig -import com.lambda.interaction.rotation.RotationMode +import com.lambda.config.groups.RotationSettings import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.util.math.MathUtils.random object Baritone : Module( name = "Baritone", description = "Baritone configuration", defaultTags = setOf(ModuleTag.CLIENT) ) { - private val r1 by setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1) - private val r2 by setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1) - - val rotation = object : IRotationConfig { - override val rotationMode = RotationMode.SYNC - override val turnSpeed get() = random(r1, r2) - override val keepTicks = 3 - override val resetTicks = 3 - } + val rotation = RotationSettings(this) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt b/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt new file mode 100644 index 000000000..4e336bde6 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt @@ -0,0 +1,35 @@ +package com.lambda.module.modules.client + +import com.lambda.config.groups.InteractionSettings +import com.lambda.config.groups.BuildSettings +import com.lambda.config.groups.RotationSettings +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.util.BlockUtils.allSigns +import net.minecraft.block.Block + +object TaskFlow : Module( + name = "TaskFlow", + description = "Settings for task automation", + defaultTags = setOf(ModuleTag.CLIENT, ModuleTag.AUTOMATION) +) { + enum class Page { + BUILD, ROTATION, INTERACTION, TASKS + } + + private val page by setting("Page", Page.BUILD) + val build = BuildSettings(this) { + page == Page.BUILD + } + val rotation = RotationSettings(this) { + page == Page.ROTATION + } + val interact = InteractionSettings(this) { + page == Page.INTERACTION + } + val taskCooldown by setting("Task Cooldown", 0, 0..10000, 10, unit = " ms") { + page == Page.TASKS + } +// val disposables by setting("Disposables", ItemUtils.defaultDisposables) + val ignoredBlocks = mutableSetOf().apply { addAll(allSigns) } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt index 6bcb86428..b7401ca1b 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt @@ -1,7 +1,7 @@ package com.lambda.module.modules.combat -import com.lambda.config.InteractionSettings -import com.lambda.config.RotationSettings +import com.lambda.config.groups.InteractionSettings +import com.lambda.config.groups.RotationSettings import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.concurrentListener import com.lambda.event.listener.SafeListener.Companion.listener diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt new file mode 100644 index 000000000..dfb594d34 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt @@ -0,0 +1,28 @@ +package com.lambda.module.modules.debug + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.task.tasks.AcquireMaterial.Companion.acquire +import com.lambda.util.Communication.info +import net.minecraft.item.Items + +object ContainerTest : Module( + name = "ContainerTest", + description = "Test container", + defaultTags = setOf(ModuleTag.DEBUG) +) { + init { + listener { +// info(task.info) + } + + onEnable { + acquire { + Items.OBSIDIAN.select() + }.start(null) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt index 1c0fac102..da1b5ee09 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/debug/InventoryDebug.kt @@ -5,6 +5,7 @@ import com.lambda.event.events.PacketEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.util.Communication.info import com.lambda.util.DynamicReflectionSerializer.dynamicString import net.minecraft.network.packet.c2s.play.* import net.minecraft.network.packet.s2c.play.InventoryS2CPacket @@ -19,7 +20,7 @@ object InventoryDebug : Module( listener { when (val packet = it.packet) { is UpdateSelectedSlotS2CPacket, is InventoryS2CPacket -> { - LOG.info(packet.dynamicString()) + this@InventoryDebug.info(packet.dynamicString()) } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt deleted file mode 100644 index cdfb981b6..000000000 --- a/common/src/main/kotlin/com/lambda/module/modules/debug/RotationTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.lambda.module.modules.debug - -import com.lambda.config.InteractionSettings -import com.lambda.config.RotationSettings -import com.lambda.event.events.RotationEvent -import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.module.Module -import com.lambda.module.tag.ModuleTag -import net.minecraft.util.Hand -import net.minecraft.util.hit.BlockHitResult -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Direction - -object RotationTest : Module( - name = "RotationTest", - description = "Test rotation", - defaultTags = setOf(ModuleTag.DEBUG), -) { - private val rotationConfig = RotationSettings(this) - private val interactionConfig = InteractionSettings(this) - - private var pos: BlockPos = BlockPos.ORIGIN - private var side = Direction.UP - - init { - onEnable { - val hit = mc.crosshairTarget as? BlockHitResult ?: return@onEnable - pos = hit.blockPos - side = hit.side - } - - listener { event -> -// val target = getClosestEntity( -// player.eyePos, interaction.reachDistance.toDouble() -// ) ?: return@listener - - event.lookAtBlock(rotationConfig, interactionConfig, pos, setOf(side)) - } - - listener { - (mc.crosshairTarget as? BlockHitResult)?.let { hit -> - if (hit.blockPos != pos || hit.side != side) { - interaction.cancelBlockBreaking() - return@listener - } - - interaction.updateBlockBreakingProgress(pos, side) - player.swingHand(Hand.MAIN_HAND) - return@listener - } - - interaction.cancelBlockBreaking() - } - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt index 4a2006399..cc9dcb725 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt @@ -17,12 +17,12 @@ object PacketLimiter : Module( defaultTags = setOf(ModuleTag.NETWORK) ) { private var packetQueue = LimitedDecayQueue(99, 1000) - private val limit by setting("Limit", 99, 1..100, 1, "The maximum amount of packets to send per given time interval").apply { + private val limit by setting("Limit", 99, 1..100, 1, "The maximum amount of packets to send per given time interval", unit = " packets").apply { onValueChange { _, to -> packetQueue.setMaxSize(to) } } - private val interval by setting("Duration", 1000L, 1L..1000L, 50L, "The interval / duration in milliseconds to limit packets for").apply { + private val interval by setting("Duration", 1000L, 1L..1000L, 50L, "The interval / duration in milliseconds to limit packets for", unit = " ms").apply { onValueChange { _, to -> packetQueue.setInterval(to) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt index 1cfe32296..5c3667cb0 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt @@ -9,6 +9,7 @@ import com.lambda.event.listener.UnsafeListener.Companion.unsafeConcurrentListen import com.lambda.event.listener.UnsafeListener.Companion.unsafeListener import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runIO import com.lambda.util.Communication import com.lambda.util.Communication.info import com.lambda.util.DynamicReflectionSerializer.dynamicString @@ -70,7 +71,7 @@ object PacketLogger : Module( private val File.relativePath: Path get() = mc.runDirectory.toPath().relativize(toPath()) init { - lambdaScope.launch(Dispatchers.IO) { + runIO { storageFlow.collect { entry -> file?.appendText(entry) if (logToChat) this@PacketLogger.info(entry) diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt b/common/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt index 112e77b20..35dbc8eb7 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt @@ -2,7 +2,7 @@ package com.lambda.module.modules.player import baritone.utils.PlayerMovementInput import com.lambda.Lambda.mc -import com.lambda.config.RotationSettings +import com.lambda.config.groups.RotationSettings import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RenderEvent @@ -22,6 +22,7 @@ import com.lambda.util.primitives.extension.rotation import com.lambda.util.world.raycast.RayCastUtils.orMiss import com.lambda.util.world.raycast.RayCastUtils.orNull import net.minecraft.client.input.KeyboardInput +import net.minecraft.client.option.Perspective import net.minecraft.entity.Entity import net.minecraft.util.math.Vec3d @@ -32,18 +33,20 @@ object Freecam : Module( ) { private val speed by setting("Speed", 0.5f, 0.1f..1.0f, 0.1f) private val sprint by setting("Sprint Multiplier", 3.0f, 0.1f..10.0f, 0.1f, description = "Set below 1.0 to fly slower on sprint.") + private val reach by setting("Reach", 10.0, 1.0..100.0, 1.0, "Freecam reach distance") private val rotateToTarget by setting("Rotate to target", true) - private val reach by setting("Reach", 4.0, 1.0..100.0, 0.1) - private val rotationConfig = RotationSettings(this).apply { + private val rotationConfig = RotationSettings(this) { + rotateToTarget + }.apply { rotationMode = RotationMode.LOCK } + private var lastPerspective = Perspective.FIRST_PERSON private var prevPosition: Vec3d = Vec3d.ZERO private var position: Vec3d = Vec3d.ZERO private val interpolatedPosition: Vec3d - get() = - prevPosition.interpolate(position, mc.partialTicks) + get() = prevPosition.interpolate(position, mc.partialTicks) private var rotation: Rotation = Rotation.ZERO private var velocity: Vec3d = Vec3d.ZERO @@ -68,11 +71,17 @@ object Freecam : Module( init { onEnable { + lastPerspective = mc.options.perspective + mc.options.perspective = Perspective.FIRST_PERSON position = player.eyePos rotation = player.rotation velocity = Vec3d.ZERO } + onDisable { + mc.options.perspective = lastPerspective + } + listener(Int.MAX_VALUE) { if (!rotateToTarget) return@listener val target = mc.crosshairTarget?.orNull ?: return@listener diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt b/common/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt new file mode 100644 index 000000000..11cf9d200 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt @@ -0,0 +1,163 @@ +package com.lambda.module.modules.player + +import com.lambda.interaction.construction.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.task.Task +import com.lambda.task.tasks.BuildStructure.Companion.buildStructure +import com.lambda.util.BaritoneUtils +import com.lambda.util.Communication.info +import com.lambda.util.player.MovementUtils.octant +import com.lambda.util.primitives.extension.Structure +import com.lambda.util.world.StructureUtils.generateDirectionalTube +import net.minecraft.block.Blocks +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.EightWayDirection +import net.minecraft.util.math.Vec3i +import kotlin.math.roundToInt + +object HighwayTools : Module( + name = "HighwayTools", + description = "Auto highway builder", + defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) +) { + private val height by setting("Height", 4, 1..10, 1) + private val width by setting("Width", 6, 1..30, 1) + private val rimHeight by setting("Rim Height", 1, 0..6, 1) + private val cornerBlock by setting("Corner Block", false, description = "Include corner blocks in the highway") + private val distance by setting("Distance", -1, -1..1000000, 1, description = "Distance to build the highway (negative for infinite)") + private val sliceSize by setting("Slice Size", 3, 1..5, 1, description = "Number of slices to build at once") + private val material by setting("Material", Blocks.OBSIDIAN, description = "Material to build the highway with") + + private var octant = EightWayDirection.NORTH + private var distanceMoved = 0 + private var startPos = BlockPos.ORIGIN + private var currentPos = BlockPos.ORIGIN + private var runningTask: Task<*>? = null + + init { + onEnable { + octant = player.octant + startPos = player.blockPos + currentPos = startPos + buildSlice() + } + onDisable { + runningTask?.cancel() + runningTask = null + distanceMoved = 0 + BaritoneUtils.cancel() + } + } + + private fun buildSlice() { + distanceMoved += sliceSize + + var structure: Structure = mutableMapOf() + val slice = highwaySlice() + repeat(sliceSize) { + val vec = Vec3i(octant.offsetX, 0, octant.offsetZ) + currentPos = currentPos.add(vec) + structure += slice.map { it.key.add(currentPos) to it.value } + } + + runningTask = buildStructure { + structure.toBlueprint() + }.apply { + onSuccess { _, _ -> + if (distanceMoved < distance || distance < 0) { + buildSlice() + } else { + this@HighwayTools.info("Highway built") + disable() + } + } + onFailure { _, _ -> + disable() + } + start(null) + } + } + + private fun highwaySlice(): Structure { + val structure = mutableMapOf() + val orthogonal = EightWayDirection.entries[(octant.ordinal + 2).mod(8)] + val center = (width / 2.0).roundToInt() + + // Area to clear + structure += generateDirectionalTube( + orthogonal, + width, + height, + -center, + -1, + ).associateWith { TargetState.Air } + + // Highway + structure += generateDirectionalTube( + orthogonal, + width, + 1, + -center, + -1, + ).associateWith { TargetState.Block(material) } + + // Left rim + structure += generateDirectionalTube( + orthogonal, + 1, + rimHeight, + -center + width - 1, + 0, + ).associateWith { TargetState.Block(material) } + + // Right rim + structure += generateDirectionalTube( + orthogonal, + 1, + rimHeight, + -center, + 0, + ).associateWith { TargetState.Block(material) } + + if (!cornerBlock) { + structure -= generateDirectionalTube( + orthogonal, + 1, + 1, + -center + width - 1, + -1, + ) + + structure -= generateDirectionalTube( + orthogonal, + 1, + 1, + -center, + -1, + ) + } + + // Support for the left corner + structure += generateDirectionalTube( + orthogonal, + 1, + 1, + -center + width - 1, + -1, + ).associateWith { TargetState.Solid } + + // Support for the right corner + structure += generateDirectionalTube( + orthogonal, + 1, + 1, + -center, + -1, + ).associateWith { TargetState.Solid } + + return structure + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt b/common/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt index 59e08dcff..fc5da0fed 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt @@ -1,42 +1,68 @@ package com.lambda.module.modules.player -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.interaction.visibilty.VisibilityChecker.getVisibleSurfaces +import com.lambda.interaction.construction.DynamicBlueprint.Companion.blueprintOnTick +import com.lambda.interaction.construction.verify.TargetState import com.lambda.module.Module import com.lambda.module.tag.ModuleTag -import com.lambda.util.Communication.info -import com.lambda.util.math.VecUtils.dist -import com.lambda.util.world.WorldUtils.searchBlocks -import net.minecraft.util.math.Vec3i +import com.lambda.task.Task.Companion.emptyTask +import com.lambda.task.tasks.BuildStructure.Companion.buildStructure +import com.lambda.util.BlockUtils.blockPos +import com.lambda.util.BlockUtils.blockState +import net.minecraft.util.math.BlockPos object Nuker : Module( name = "Nuker", description = "Breaks blocks around you", defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) ) { + private val height by setting("Height", 4, 1..8, 1) + private val width by setting("Width", 4, 1..8, 1) private val flatten by setting("Flatten", true) + private val onlyBreakInstant by setting("Only Break Instant", true) + private val fillFloor by setting("Fill Floor", false) - private val range = Vec3i(4, 4, 4) // TODO: Customizable + private var task = emptyTask() init { - listener { - searchBlocks(player.blockPos, range, null, - iterator = { state, pos, _ -> - state.getCollisionShape(world, pos).boundingBox - .getVisibleSurfaces(player.eyePos).firstOrNull()?.let { - interaction.updateBlockBreakingProgress(pos, it) // Crashes if the time to mine is not instant ?? - } - this@Nuker.info("Breaking ${state.block.name.string} at $pos with hardness ${state.getHardness(world, pos)}") - }, - ) { state, pos -> - state.isSolidBlock(world, pos) - && !state.isAir - && (!flatten || pos.y >= player.y) - //&& state.getHardness(world, pos) <= 1.0f - && state.getHardness(world, pos) > 0.0f - && player.eyePos dist pos.toCenterPos() <= interaction.reachDistance - 1 + onEnable { + task = buildStructure( + pathing = false, + finishOnDone = false, + cancelOnUnsolvable = false + ) { + blueprintOnTick { _ -> + val selection = BlockPos.iterateOutwards(player.blockPos, width, height, width) + .asSequence() + .map { it.blockPos } + .filter { !world.isAir(it) } + .filter { !flatten || it.y >= player.blockPos.y } + .filter { !onlyBreakInstant || it.blockState(world).getHardness(world, it) <= 1 } + .filter { it.blockState(world).getHardness(world, it) >= 0 } + .associateWith { TargetState.Air } + + if (fillFloor) { + val floor = BlockPos.iterateOutwards(player.blockPos.down(), width, 0, width) + .map { it.blockPos } + .associateWith { TargetState.Solid } + return@blueprintOnTick selection + floor + } + + selection + } } + task.start(null) } + + onDisable { + task.cancel() + } + +// listener { +// task?.let { +// if (!it.isRunning) return@listener +// +// info(it.info) +// } +// } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt index f055d86e3..6bc52bd3c 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -2,7 +2,7 @@ package com.lambda.module.modules.player import com.google.gson.* import com.lambda.brigadier.CommandResult -import com.lambda.config.RotationSettings +import com.lambda.config.groups.RotationSettings import com.lambda.context.SafeContext import com.lambda.core.TimerManager import com.lambda.event.EventFlow.lambdaScope @@ -56,15 +56,14 @@ object Replay : Module( private val cycle by setting("Cycle Play Mode", KeyCode.B, description = "REPLAY: Replay the recording once. CONTINUE: Replay the recording and continue recording. LOOP: Loop the recording.") private val check by setting("Set Checkpoint", KeyCode.V, description = "Create a checkpoint while recording.") - private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = "repeats") + private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = " repeats") private val velocityCheck by setting("Velocity check", true, description = "Check if the player is moving before starting a recording.") private val cancelOnDeviation by setting("Cancel on deviation", true) private val deviationThreshold by setting("Deviation threshold", 0.1, 0.1..5.0, 0.1, description = "The threshold for the deviation to cancel the replay.") { cancelOnDeviation } private val rotationConfig = RotationSettings(this).apply { rotationMode = RotationMode.LOCK - r1 = 1000.0 - r2 = 1001.0 + instant = true } enum class State { diff --git a/common/src/main/kotlin/com/lambda/task/RootTask.kt b/common/src/main/kotlin/com/lambda/task/RootTask.kt new file mode 100644 index 000000000..c4213b712 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/RootTask.kt @@ -0,0 +1,12 @@ +package com.lambda.task + +object RootTask : Task() { + init { + name = "RootTask" + } + + fun addInfo(debugText: MutableList) { + debugText.add("") + debugText.addAll(info.string.split("\n")) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/Task.kt b/common/src/main/kotlin/com/lambda/task/Task.kt index e36c59e5c..3b1be0c76 100644 --- a/common/src/main/kotlin/com/lambda/task/Task.kt +++ b/common/src/main/kotlin/com/lambda/task/Task.kt @@ -2,155 +2,282 @@ package com.lambda.task import com.lambda.Lambda.LOG import com.lambda.context.SafeContext +import com.lambda.event.Event import com.lambda.event.EventFlow import com.lambda.event.Subscriber +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.module.modules.client.TaskFlow +import com.lambda.threading.runConcurrent +import com.lambda.threading.runGameScheduled import com.lambda.threading.runSafe +import com.lambda.util.BaritoneUtils +import com.lambda.util.Communication.logError +import com.lambda.util.Communication.warn +import com.lambda.util.DynamicReflectionSerializer.dynamicString import com.lambda.util.Nameable -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.TimeoutCancellationException +import com.lambda.util.text.buildText +import com.lambda.util.text.color +import com.lambda.util.text.literal +import com.lambda.util.text.text import kotlinx.coroutines.delay -import kotlinx.coroutines.withTimeout +import net.minecraft.text.Text +import org.apache.commons.lang3.time.DurationFormatUtils +import java.awt.Color /** - * A [Task] represents a time-critical activity that executes a suspending action function. - * It is designed to automate in-game activities without the need for strict event-based programming, - * thanks to the use of suspending functions which allow for linear coding. + * A [Task] represents a time-critical activity. + * It automates in-game activities through event-based programming, leveraging game events to control task flow. * * [Result] is the type of the result that the task will return when it completes successfully. * In case the task should not return any result, [Unit] can be used as the type. * - * A [Task] can have event listeners, but they are only active while the action function is running. + * A [Task] can have event listeners that are active while the task is running. + * The task will attempt to execute its subtasks sequentially, and can either keep listening + * or stop receiving events while the subtasks are running. * * It supports a builder pattern, allowing you to chain configuration methods like [withDelay], * [withTimeout], [withMaxAttempts], [withRepeats], [onSuccess], [onRetry], [onTimeout], [onFailure], and [onRepeat] * to construct a [Task] instance. * This makes it easy to build complex flows of nested tasks. * - * CAUTION: When implementing the [onAction] function, - * ensure that the function adheres to thread safety measures. - * This includes avoiding write operations on non-synchronized in-game data - * unless explicitly running on the game thread using `runSafeOnGameThread { ... }`. - * * @property delay The delay before the task starts, in milliseconds. - * @property timeout The maximum time that the task is allowed to run, in milliseconds. - * @property maxAttempts The maximum number of attempts to execute the task before it is considered failed. - * @property repeats The number of times the task should be repeated. + * @property timeout The maximum time that the task is allowed to run, in ticks. + * @property tries The maximum number of attempts to execute the task before it is considered failed. + * @property executions The number of times the task should be repeated. * @property onSuccess The action to be performed when the task completes successfully. * @property onRetry The action to be performed when the task is retried after a failure or timeout. * @property onTimeout The action to be performed when the task times out. * @property onRepeat The action to be performed each time the task is repeated. * @property onException The action to be performed when the task encounters an exception. */ -abstract class Task( - private var delay: Long = 0L, - private var timeout: Long = Long.MAX_VALUE, - private var maxAttempts: Int = 1, - private var repeats: Int = 1, - private var onSuccess: (suspend (Result) -> Unit) = {}, - private var onRetry: (suspend Task.() -> Unit) = {}, - private var onTimeout: (suspend Task.() -> Unit) = {}, - private var onRepeat: (suspend Task.(Int) -> Unit) = {}, - private var onException: (suspend Task.(Throwable) -> Unit) = {}, -) : Nameable { - private val creationTimestamp = System.currentTimeMillis() - private val age: Long get() = System.currentTimeMillis() - creationTimestamp - override val name: String get() = this::class.simpleName ?: "Task" +abstract class Task : Nameable { + open var delay: Int = 0 + open var timeout: Int = Int.MAX_VALUE + open var tries: Int = 0 + open var repeats: Int = 0 + open var cooldown: Int = TaskFlow.taskCooldown + open var onStart: SafeContext.(Task) -> Unit = {} + open var onSuccess: SafeContext.(Task, Result) -> Unit = { _, _ -> } + open var onRetry: SafeContext.(Task) -> Unit = {} + open var onTimeout: SafeContext.(Task) -> Unit = {} + open var onRepeat: SafeContext.(Task, Result, Int) -> Unit = { _, _, _ -> } + open var onException: SafeContext.(Task, Throwable) -> Unit = { _, _ -> } + + open var pausable = true + + var parent: Task<*>? = null + private val root: Task<*> get() = parent?.root ?: this + private val depth: Int get() = parent?.depth?.plus(1) ?: 0 + + private var executions = 0 + private var attempted = 0 + private val subTasks = mutableListOf>() + private var state = State.IDLE + var age = 0 + + private val isDeactivated get() = state == State.DEACTIVATED + val isActivated get() = state == State.ACTIVATED + val isRunning get() = state == State.ACTIVATED || state == State.DEACTIVATED + val isFailed get() = state == State.FAILED + val isCompleted get() = state == State.COMPLETED || state == State.COOLDOWN + val isRoot get() = parent == null + override var name = this::class.simpleName ?: "Task" + val identifier get() = "$name@${hashCode()}" + + // ToDo: Better color management + private val primaryColor = Color(0, 255, 0, 100) val syncListeners = Subscriber() private val concurrentListeners = Subscriber() - /** - * Executes the main action of the task. - * - * This function should only perform read operations on the game thread due to potential concurrency issues. - * If write operations are necessary, they should be wrapped in the `runSafeOnGameThread { ... }` function to ensure thread safety. - * - * @return The result of the action, of type [Result]. - */ - abstract suspend fun SafeContext.onAction(): Result + enum class State { + IDLE, + ACTIVATED, + DEACTIVATED, + CANCELLED, + FAILED, + COOLDOWN, + COMPLETED, + } - /** - * This function is called when the task is canceled. - * It should be overridden for tasks that need to perform cleanup operations, - * such as cancelling a block breaking progress, releasing resources, - * or stopping any ongoing operations that were started by the task. - */ - open fun SafeContext.onCancel() {} + operator fun plus(other: Task<*>) = subTasks.add(other) + + init { + listener { + parent?.let { + it.age++ + } + if (++age >= timeout) { + onTimeout(this@Task) + failure(TimeoutException(age, attempted)) + } + } + } /** - * Executes the task with the configured parameters. - * - * This function will attempt to execute the task for a specified number of attempts and repeats. - * It handles delays before task execution, task timeouts, and task cancellations. - * It also manages the lifecycle of event listeners associated with the task. - * - * @return The result of the task execution, represented as a [TaskResult]. - * This could be a successful result ([TaskResult.Success]), a cancellation ([TaskResult.Cancelled]), - * a failure ([TaskResult.Failure]) due to an exception, or a timeout - * ([TaskResult.Timeout]). + * "Typo" in name is used to force the dsl style green + * (color is based on name hash, don't ask me who came up with this) */ - suspend fun execute(): TaskResult { - var attempt = 1 - var iteration = 0 - - do { - if (delay > 0) { - LOG.info("Delaying task $name for $delay ms") - delay(delay) - } + @DslMarker + annotation class Ta5kBuilder - startListening() + @Ta5kBuilder + open fun SafeContext.onStart() {} - try { - withTimeout(timeout) { - runSafe { - onAction() - } - }?.let { result -> - iteration++ + @Ta5kBuilder + fun start(parent: Task<*>?, pauseParent: Boolean = true): Task { + executions++ + val owner = parent ?: RootTask + owner.subTasks.add(this) - if (iteration == repeats) { - onSuccess(result) - tidyUp() - return TaskResult.Success(result) - } + LOG.info("${owner.identifier} started $identifier") + this.parent = owner + if (pauseParent && owner.isActivated && !owner.isRoot) { + LOG.info("$identifier deactivating parent ${owner.identifier}") + owner.deactivate() + } + + activate() + runSafe { + onStart(this@Task) + onStart() + } + return this + } - onRepeat(iteration) + @Ta5kBuilder + fun activate() { + if (isActivated) return + state = State.ACTIVATED + startListening() + } + + @Ta5kBuilder + fun deactivate() { + if (isDeactivated) return + state = State.DEACTIVATED + stopListening() + } + + @Ta5kBuilder + fun SafeContext.success(result: Result) { + if (executions < repeats) { + executions++ + LOG.info("Repeating $identifier $executions/$repeats...") + onRepeat(this@Task, result, executions) + reset() + return + } + + stopListening() + if (cooldown > 0) { + state = State.COOLDOWN + runConcurrent { + delay(cooldown.toLong()) + runGameScheduled { + if (state == State.COOLDOWN) finish(result) } - } catch (e: TimeoutCancellationException) { - attempt++ - LOG.warn("Task $name timed out after $age ms and $attempt attempts") - onRetry() - tidyUp() - } catch (e: CancellationException) { - tidyUp() - LOG.warn("Coroutine for task $name cancelled") - return TaskResult.Cancelled - } catch (e: Throwable) { - attempt++ - LOG.error("Task $name failed after $age ms and $attempt attempts", e) - onException(e) - tidyUp() - return TaskResult.Failure(e) } + } else finish(result) + } - tidyUp() - } while (attempt < maxAttempts || iteration <= repeats) + private fun SafeContext.finish(result: Result) { + state = State.COMPLETED + try { + onSuccess(this@Task, result) + } catch (e: ClassCastException) { + result?.let { + LOG.error("Failed to cast result of $identifier to ${it::class.simpleName}") + } + failure(e) + } - LOG.error("Task $name fully timed out after $age ms and $attempt attempts") - onTimeout() - return TaskResult.Timeout(timeout, attempt) + notifyParent() + LOG.info("$identifier completed successfully after $attempted retries and $executions executions.") } - private fun tidyUp() { - runSafe { - onCancel() + @Ta5kBuilder + fun cancel() { + if (state == State.CANCELLED) return + + cancelSubTasks() + state = State.CANCELLED + stopListening() + runSafe { onCancel() } + LOG.info("$identifier was cancelled") + } + + @Ta5kBuilder + fun cancelSubTasks() { + subTasks.forEach { it.cancel() } + } + + @Ta5kBuilder + fun failure(message: String) { + failure(IllegalStateException(message)) + } + + @Ta5kBuilder + fun failure( + e: Throwable, + stacktrace: MutableList> = mutableListOf() + ) { + if (attempted < tries) { + attempted++ + warn("Failed task with error: ${e.message}, retrying ($attempted/$tries) ...") + runSafe { + onRetry(this@Task) + } + reset() + return } - EventFlow.syncListeners.unsubscribe(syncListeners) - EventFlow.concurrentListeners.unsubscribe(concurrentListeners) + state = State.FAILED + stopListening() + runSafe { onException(this@Task, e) } + stacktrace.add(this) + parent?.failure(e, stacktrace) ?: run { + val message = buildString { + stacktrace.firstOrNull()?.let { first -> + append("${first.identifier} failed: ${e.message}\n") + stacktrace.drop(1).forEach { + append(" -> ${it.identifier}\n") + } + } + } + LOG.error(message, e) + logError(message) + } + } + + /** + * This function is called when the task is canceled. + * It should be overridden for tasks that need to perform cleanup operations, + * such as cancelling a block breaking progress, releasing resources, + * or stopping any ongoing operations that were started by the task. + */ + @Ta5kBuilder + open fun SafeContext.onCancel() {} - // ToDo: cancel baritone + private fun notifyParent() { + parent?.let { par -> + when { + par.isCompleted -> { + LOG.info("$identifier completed parent ${par.identifier}") + par.notifyParent() + } + !par.isActivated -> { + LOG.info("$identifier reactivated parent ${par.identifier}") + par.activate() + } + } + } + } + + @Ta5kBuilder + private fun reset() { + age = 0 } private fun startListening() { @@ -158,19 +285,19 @@ abstract class Task( EventFlow.concurrentListeners.subscribe(concurrentListeners) } - fun cancel() { - runSafe { - onCancel() - } + private fun stopListening() { + EventFlow.syncListeners.unsubscribe(syncListeners) + EventFlow.concurrentListeners.unsubscribe(concurrentListeners) } /** * Sets the delay before the task starts. * - * @param delay The delay in milliseconds. + * @param delay The delay in ticks. * @return This task instance with the updated delay. */ - fun withDelay(delay: Long): Task { + @Ta5kBuilder + fun withDelay(delay: Int): Task { this.delay = delay return this } @@ -178,22 +305,38 @@ abstract class Task( /** * Sets the timeout for a single attempt of the task * - * @param timeout The timeout in milliseconds + * @param timeout The timeout in ticks. * @return This task instance with the updated timeout. */ - fun withTimeout(timeout: Long): Task { + @Ta5kBuilder + fun withTimeout(timeout: Int): Task { this.timeout = timeout return this } + /** + * Sets the cooldown period for the task. + * + * The cooldown period is the time that the task will wait before it the next task can be executed. + * + * @param cooldown The cooldown period in milliseconds. + * @return This task instance with the updated cooldown period. + */ + @Ta5kBuilder + fun withCooldown(cooldown: Int): Task { + this.cooldown = cooldown + return this + } + /** * Sets the maximum number of attempts to execute the task before it is considered failed. * * @param maxAttempts The maximum number of attempts. * @return This task instance with the updated maximum attempts. */ + @Ta5kBuilder fun withMaxAttempts(maxAttempts: Int): Task { - this.maxAttempts = maxAttempts + this.tries = maxAttempts return this } @@ -203,29 +346,60 @@ abstract class Task( * @param repeats The number of repeats. * @return This task instance with the updated number of repeats. */ + @Ta5kBuilder fun withRepeats(repeats: Int): Task { this.repeats = repeats return this } + /** + * Sets the action to be performed when the task starts. + * + * @param action The action to be performed. + * @return The task instance with the updated start action. + */ + @Ta5kBuilder + fun onStart(action: SafeContext.(Task) -> Unit): Task { + this.onStart = action + return this + } + /** * Sets the action to be performed when the task completes successfully. * * @param action The action to be performed. * @return The task instance with the updated success action. */ - fun onSuccess(action: suspend (Result) -> Unit): Task { + @Ta5kBuilder + fun onSuccess(action: SafeContext.(Task, Result) -> Unit): Task { this.onSuccess = action return this } + /** + * This method allows you to chain another task that will be started upon the successful completion of the current task. + * The action provided as a parameter will be used to create the next task. + * The context of the action, the current task, and the result of the current task are passed as parameters to the action. + * + * @param action A lambda function that takes the current task and its result as parameters and returns the next task to be started. + * @return The current task instance with the updated success action. + */ + @Ta5kBuilder + fun thenRun(action: SafeContext.(Task, Result) -> Task<*>): Task { + this.onSuccess = { task, result -> + action(this, task, result).start(task) + } + return this + } + /** * Sets the action to be performed when the task is retried after a failure or timeout. * * @param action The action to be performed. * @return The task instance with the updated retry action. */ - fun onRetry(action: suspend Task.() -> Unit): Task { + @Ta5kBuilder + fun onRetry(action: SafeContext.(Task) -> Unit): Task { this.onRetry = action return this } @@ -236,7 +410,8 @@ abstract class Task( * @param action The action to be performed. * @return The task instance with the updated timeout action. */ - fun onTimeout(action: suspend Task.() -> Unit): Task { + @Ta5kBuilder + fun onTimeout(action: SafeContext.(Task) -> Unit): Task { this.onTimeout = action return this } @@ -247,7 +422,8 @@ abstract class Task( * @param action The action to be performed. * @return The task instance with the updated exception action. */ - fun onFailure(action: suspend Task.(Throwable) -> Unit): Task { + @Ta5kBuilder + fun onFailure(action: SafeContext.(Task, Throwable) -> Unit): Task { this.onException = action return this } @@ -258,8 +434,118 @@ abstract class Task( * @param action The action to be performed. * @return The task instance with the updated repeat action. */ - fun onRepeat(action: suspend Task.(Int) -> Unit): Task { + @Ta5kBuilder + fun onRepeat(action: SafeContext.(Task, Result, Int) -> Unit): Task { this.onRepeat = action return this } + + @Ta5kBuilder + inline fun withListener( + crossinline action: SafeContext.(Task) -> Unit + ): Task { + listener { + action(this@Task) + } + return this + } + + @Ta5kBuilder + fun withName(name: String): Task { + this.name = name + return this + } + + class TimeoutException(age: Int, attempts: Int) : Exception("Task timed out after $age ticks and $attempts attempts") + + companion object { + val MAX_DEPTH = 20 + const val MAX_DEBUG_ENTRIES = 15 + + @Ta5kBuilder + fun emptyTask( + name: String = "EmptyTask", + ): Task = object : Task() { + init { this.name = name } + override fun SafeContext.onStart() { success(Unit) } + } + + @Ta5kBuilder + fun failTask( + message: String + ): Task = object : Task() { + init { this.name = "FailTask" } + override fun SafeContext.onStart() { + failure(message) + } + } + + @Ta5kBuilder + fun failTask( + e: Throwable + ): Task = object : Task() { + init { this.name = "FailTask" } + override fun SafeContext.onStart() { + failure(e) + } + } + + @Ta5kBuilder + fun buildTask( + name: String = "Task", + block: SafeContext.() -> Unit + ) = object : Task() { + init { this.name = name } + override fun SafeContext.onStart() { + try { + success(block()) + } catch (e: Throwable) { + failure(e) + } + } + } + + @Ta5kBuilder + inline fun buildTaskWithReturn( + crossinline block: SafeContext.() -> R + ) = object : Task() { + override fun SafeContext.onStart() { + try { + success(block()) + } catch (e: Throwable) { + failure(e) + } + } + } + } + + val info: Text + get() = buildText { + literal("Name ") + color(primaryColor) { literal(name) } + + literal(" State ") + color(primaryColor) { literal(state.name) } + + literal(" Runtime ") + color(primaryColor) { + literal(DurationFormatUtils.formatDuration(age * 50L, "HH:mm:ss,SSS").dropLast(1)) + } + + val display = subTasks.reversed().take(MAX_DEBUG_ENTRIES) + display.forEach { + literal("\n${" ".repeat(depth + 1)}") + text(it.info) + } + + val left = subTasks.size - display.size + if (left > 0) { + literal("\n${" ".repeat(depth + 1)}And ") + color(primaryColor) { + literal("$left") + } + literal(" more...") + } + + } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/TaskDecisionTree.kt b/common/src/main/kotlin/com/lambda/task/TaskDecisionTree.kt new file mode 100644 index 000000000..1f8f3313e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/TaskDecisionTree.kt @@ -0,0 +1,5 @@ +package com.lambda.task + +class TaskDecisionTree { + +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt b/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt new file mode 100644 index 000000000..008c3d333 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt @@ -0,0 +1,26 @@ +package com.lambda.task.tasks + +import com.lambda.context.SafeContext +import com.lambda.interaction.material.ContainerManager +import com.lambda.interaction.material.ContainerManager.findContainerWithSelection +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task + +class AcquireMaterial( + val selection: StackSelection +) : Task() { + override fun SafeContext.onStart() { + findContainerWithSelection(selection) + ?.withdraw(selection) + ?.onSuccess { _, _ -> + success(selection) + }?.start(this@AcquireMaterial) + ?: failure(ContainerManager.NoContainerFound(selection)) // ToDo: Create crafting path + } + + companion object { + @Ta5kBuilder + fun acquire(selection: () -> StackSelection) = + AcquireMaterial(selection()) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt b/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt new file mode 100644 index 000000000..6d91f1906 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt @@ -0,0 +1,147 @@ +package com.lambda.task.tasks + +import baritone.api.pathing.goals.GoalBlock +import com.lambda.context.SafeContext +import com.lambda.event.events.RotationEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.config.groups.InteractionConfig +import com.lambda.interaction.construction.context.BreakContext +import com.lambda.config.groups.IRotationConfig +import com.lambda.interaction.visibilty.VisibilityChecker.lookAtBlock +import com.lambda.module.modules.client.TaskFlow +import com.lambda.task.Task +import com.lambda.task.tasks.GoalTask.Companion.moveToBlock +import com.lambda.task.tasks.GoalTask.Companion.moveToBlockUntil +import com.lambda.task.tasks.GoalTask.Companion.moveToGoal +import com.lambda.task.tasks.GoalTask.Companion.moveToGoalUntil +import com.lambda.util.BaritoneUtils +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.item +import com.lambda.util.item.ItemUtils.defaultDisposables +import com.lambda.util.player.SlotUtils.clickSlot +import com.lambda.util.player.SlotUtils.hotbarAndStorage +import com.lambda.util.primitives.extension.inventorySlots +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.block.BlockState +import net.minecraft.entity.ItemEntity +import net.minecraft.screen.slot.SlotActionType +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +class BreakBlock @Ta5kBuilder constructor( + private val ctx: BreakContext, + private val rotationConfig: IRotationConfig, + private val interactionConfig: InteractionConfig, + private val sides: Set, + private var collectDrop: Boolean, + private val rotate: Boolean, + private val swingHand: Boolean, +) : Task() { + val blockPos: BlockPos get() = ctx.result.blockPos + private var beginState: BlockState? = null + val SafeContext.state: BlockState get() = blockPos.blockState(world) + override var cooldown = Int.MAX_VALUE + get() = maxOf(TaskFlow.build.breakCoolDown, TaskFlow.taskCooldown) + private var drop: ItemEntity? = null + + override fun SafeContext.onStart() { + parent?.let { + if (it is BuildStructure) { + collectDrop = it.collectDrops + } + } + + if (state.isAir && !collectDrop) { + success(null) + return + } + beginState = state + } + + init { + listener { event -> + if (!rotate) return@listener + event.context = lookAtBlock(blockPos, rotationConfig, interactionConfig, sides) + } + + listener { + if (!rotate) return@listener + if (!it.context.isValid) return@listener + val hitResult = it.context.hitResult?.blockResult ?: return@listener + + breakBlock(hitResult.side) + } + + listener { + drop?.let { itemDrop -> + if (!world.entities.contains(itemDrop)) { + success(itemDrop) + return@listener + } + + if (player.hotbarAndStorage.none { it.isEmpty }) { + player.currentScreenHandler.inventorySlots.firstOrNull { + it.stack.item in defaultDisposables + }?.let { + clickSlot(it.index, 1, SlotActionType.THROW) + } + } + + BaritoneUtils.setGoalAndPath(GoalBlock(itemDrop.blockPos)) + } + + if (finish()) { + success(null) + return@listener + } + + if (rotate) return@listener + + breakBlock(ctx.result.side) + if (finish()) success(null) + } + + listener { + if (collectDrop + && drop == null + && it.entity is ItemEntity + && it.entity.pos.isInRange(blockPos.toCenterPos(), 1.0) +// && it.entity.stack.item == beginState?.block?.item // ToDo: The item entities are all air?? + ) { + drop = it.entity + } + } + } + + private fun SafeContext.finish() = state.isAir && !collectDrop + + private fun SafeContext.breakBlock(side: Direction) { + if (interaction.updateBlockBreakingProgress(blockPos, side)) { + if (player.isCreative) interaction.blockBreakingCooldown = 0 + if (swingHand) player.swingHand(ctx.hand) + } + } + + companion object { + @Ta5kBuilder + fun breakBlock( + ctx: BreakContext, + rotationConfig: IRotationConfig = TaskFlow.rotation, + interactionConfig: InteractionConfig = TaskFlow.interact, + sides: Set = emptySet(), + collectDrop: Boolean = TaskFlow.build.collectDrops, + rotate: Boolean = TaskFlow.build.rotateForBreak, + swingHand: Boolean = TaskFlow.interact.swingHand, + ) = BreakBlock( + ctx, + rotationConfig, + interactionConfig, + sides, + collectDrop, + rotate, + swingHand + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/BuildStructure.kt b/common/src/main/kotlin/com/lambda/task/tasks/BuildStructure.kt new file mode 100644 index 000000000..539baaf3c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/BuildStructure.kt @@ -0,0 +1,119 @@ +package com.lambda.task.tasks + +import baritone.api.pathing.goals.GoalNear +import com.lambda.Lambda.LOG +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.construction.Blueprint +import com.lambda.interaction.construction.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.DynamicBlueprint +import com.lambda.interaction.construction.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.result.* +import com.lambda.interaction.construction.simulation.BuildSimulator.simulate +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.module.modules.client.TaskFlow +import com.lambda.task.Task +import com.lambda.util.BaritoneUtils +import net.minecraft.util.math.BlockPos + +class BuildStructure @Ta5kBuilder constructor( + private val blueprint: Blueprint, + private val finishOnDone: Boolean = true, + private val pathing: Boolean = TaskFlow.build.pathing, + val collectDrops: Boolean = TaskFlow.build.collectDrops, + private val cancelOnUnsolvable: Boolean = true, +) : Task() { + + abstract class PathingStrategy { + + } + + override fun SafeContext.onStart() { + (blueprint as? DynamicBlueprint)?.create(this) + } + + init { + listener { + (blueprint as? DynamicBlueprint)?.update(this) + + if (finishOnDone && blueprint.structure.isEmpty()) { + failure("Structure is empty") + return@listener + } + + val results = blueprint.simulate(player.getCameraPosVec(mc.tickDelta)) + + val instantResults = results.filterIsInstance() + .filter { it.context.instantBreak } + .sorted() + .take(TaskFlow.build.breaksPerTick) + + if (TaskFlow.build.breaksPerTick > 1 && instantResults.isNotEmpty()) { + instantResults.forEach { + it.resolve.start(this@BuildStructure, pauseParent = false) + } + return@listener + } + + results.minOrNull()?.let { result -> + when (result) { + is BuildResult.Done -> checkDone() + is Resolvable -> { + LOG.info("Resolving: $result") + result.resolve.start(this@BuildStructure) + if (pathing) { + BaritoneUtils.setGoalAndPath(GoalNear(result.blockPos, 2)) + } + } + is Navigable -> { + if (pathing) BaritoneUtils.setGoalAndPath(result.goal) + } + else -> { + if (!cancelOnUnsolvable) return@listener + + failure("Failed to resolve build result: $result") + } + } + } + } + } + + private fun SafeContext.checkDone() { + if (!finishOnDone) return + success(Unit) + return + } + + companion object { + @Ta5kBuilder + fun buildStructure( + finishOnDone: Boolean = true, + collectDrops: Boolean = TaskFlow.build.collectDrops, + pathing: Boolean = TaskFlow.build.pathing, + cancelOnUnsolvable: Boolean = true, + blueprint: () -> Blueprint, + ) = BuildStructure( + blueprint(), + finishOnDone, + pathing, + collectDrops, + cancelOnUnsolvable + ) + + @Ta5kBuilder + fun breakAndCollectBlock( + blockPos: BlockPos, + ) = BuildStructure( + blockPos.toStructure(TargetState.Air).toBlueprint(), + collectDrops = true + ) + + @Ta5kBuilder + fun breakBlock( + blockPos: BlockPos, + ) = BuildStructure( + blockPos.toStructure(TargetState.Air).toBlueprint() + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/ContainerTransfer.kt b/common/src/main/kotlin/com/lambda/task/tasks/ContainerTransfer.kt new file mode 100644 index 000000000..6f6391314 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/ContainerTransfer.kt @@ -0,0 +1,20 @@ +package com.lambda.task.tasks + +import com.lambda.context.SafeContext +import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task + +class ContainerTransfer( + val selection: StackSelection, + val from: MaterialContainer, + val to: MaterialContainer +) : Task() { + override fun SafeContext.onStart() { + from.withdraw(selection).onSuccess { withdraw, _ -> + to.deposit(selection).onSuccess { _, _ -> + success(Unit) + }.start(withdraw) + }.start(this@ContainerTransfer) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/GoalTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/GoalTask.kt new file mode 100644 index 000000000..297648c7e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/GoalTask.kt @@ -0,0 +1,79 @@ +package com.lambda.task.tasks + +import baritone.api.pathing.goals.Goal +import baritone.api.pathing.goals.GoalBlock +import baritone.api.pathing.goals.GoalNear +import baritone.api.pathing.goals.GoalXZ +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.module.modules.client.TaskFlow +import com.lambda.task.Task +import com.lambda.util.BaritoneUtils +import com.lambda.util.BaritoneUtils.primary +import net.minecraft.util.math.BlockPos + +// ToDo: Custom heuristic goals +class GoalTask( + private val goal: () -> Goal, + private val check: SafeContext.() -> Boolean = { false } +) : Task() { + + init { + listener { + val goal = goal() + + primary.customGoalProcess.goal = goal + primary.customGoalProcess.path() + + if (goal.isInGoal(player.blockPos) || check()) { + primary.customGoalProcess.goal = null + success(Unit) + } + } + } + + companion object { + @Ta5kBuilder + fun moveToGoal(goal: () -> Goal) = + GoalTask(goal) + + @Ta5kBuilder + fun moveToGoalUntil(goal: () -> Goal, check: SafeContext.() -> Boolean) = + GoalTask(goal) + + @Ta5kBuilder + fun moveToGoal(goal: Goal) = + GoalTask({ goal }) + + @Ta5kBuilder + fun moveToGoalUntil(goal: Goal, check: SafeContext.() -> Boolean) = + GoalTask({ goal }, check) + + @Ta5kBuilder + fun moveToBlock(blockPos: BlockPos) = + GoalTask({ GoalBlock(blockPos) }) + + @Ta5kBuilder + fun moveNearBlock(blockPos: BlockPos, range: Int) = + GoalTask({ GoalNear(blockPos, range) }) + + @Ta5kBuilder + fun moveToBlockUntil(blockPos: BlockPos, check: SafeContext.() -> Boolean) = + GoalTask({ GoalBlock(blockPos) }, check) + + @Ta5kBuilder + fun moveToXY(blockPos: BlockPos) = + GoalTask({ GoalXZ(blockPos.x, blockPos.z) }) + + @Ta5kBuilder + fun moveUntilLoaded(blockPos: BlockPos) = + GoalTask({ GoalBlock(blockPos) }) { + world.isPosLoaded(blockPos.x, blockPos.z) + } + + @Ta5kBuilder + fun moveIntoEntityRange(blockPos: BlockPos, range: Int = 3) = + GoalTask({ GoalNear(blockPos, range) }) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/HelloWorldTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/HelloWorldTask.kt index d03aa5a14..3fcf01079 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/HelloWorldTask.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/HelloWorldTask.kt @@ -1,22 +1,23 @@ package com.lambda.task.tasks import com.lambda.Lambda.LOG -import com.lambda.context.SafeContext import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listener import com.lambda.task.Task -import kotlinx.coroutines.delay -class HelloWorldTask : Task() { +class HelloWorldTask @Ta5kBuilder constructor() : Task() { init { listener { - LOG.info("${this@HelloWorldTask::class.simpleName}: Ticking") + LOG.info("$name: ticking $age") + + if (age > 20) { + success(Unit) + } } } - override suspend fun SafeContext.onAction() { - LOG.info("Hello, World!") - delay(250) - LOG.info("Bye, World! Action completed") + companion object { + @Ta5kBuilder + fun helloWorld() = HelloWorldTask() } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt new file mode 100644 index 000000000..42ab9c0fc --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt @@ -0,0 +1,82 @@ +package com.lambda.task.tasks + +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.util.item.ItemUtils.defaultDisposables +import com.lambda.util.player.SlotUtils.clickSlot +import com.lambda.util.primitives.extension.containerSlots +import com.lambda.util.primitives.extension.inventorySlots +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType + +class InventoryTask( + val screen: H, + val selection: StackSelection, + val from: List, + val to: List, + private val closeScreen: Boolean = true +) : Task>() { + private val moved = mutableListOf() + private val selectedFrom = selection.filterSlots(from).filter { it.hasStack() } + private val selectedTo = to.filter { !it.hasStack() } + + init { + // ToDo: Needs smart code to move as efficient as possible. + // Also should handle overflow etc. Should be more generic + listener { + selectedFrom.firstOrNull()?.let { from -> + val preMove = from.stack.copy() + +// selectedTo.firstOrNull()?.let { to -> +// clickSlot(from.id, 0, SlotActionType.PICKUP) +// clickSlot(to.id, 0, SlotActionType.PICKUP) +// // ToDo: Handle overflow of cursor +// } + + // ToDo: SWAP triangle + val handler = player.currentScreenHandler + handler.inventorySlots.firstOrNull { + it.stack.item in defaultDisposables || it.stack.isEmpty + }?.let { emptySlot -> + clickSlot(emptySlot.id, 0, SlotActionType.SWAP) + clickSlot(from.id, 0, SlotActionType.SWAP) + } + moved.add(preMove) + } ?: finish() + } + + listener { + if (selectedFrom.isEmpty() || moved.sumOf { it.count } >= selection.count) { + finish() + } + } + } + + private fun SafeContext.finish() { + if (closeScreen) player.closeHandledScreen() + success(moved) + } + + companion object { + @Ta5kBuilder + inline fun moveItems( + screen: H, + selection: StackSelection, + from: List, + to: List, + ) = InventoryTask(screen, selection, from, to) + + @Ta5kBuilder + inline fun withdraw(screen: H, selection: StackSelection) = + moveItems(screen, selection, screen.containerSlots, screen.inventorySlots) + + @Ta5kBuilder + inline fun deposit(screen: H, selection: StackSelection) = + moveItems(screen, selection, screen.inventorySlots, screen.containerSlots) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt b/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt new file mode 100644 index 000000000..5ba0651e7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt @@ -0,0 +1,66 @@ +package com.lambda.task.tasks + +import com.lambda.event.events.RotationEvent +import com.lambda.event.events.ScreenHandlerEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.config.groups.InteractionConfig +import com.lambda.config.groups.IRotationConfig +import com.lambda.interaction.visibilty.VisibilityChecker.lookAtBlock +import com.lambda.module.modules.client.TaskFlow +import com.lambda.task.Task +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.screen.ScreenHandler +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +class OpenContainer( + private val blockPos: BlockPos, + private val waitForSlotLoad: Boolean = true, + private val rotationConfig: IRotationConfig = TaskFlow.rotation, + private val interactionConfig: InteractionConfig = TaskFlow.interact, + private val sides: Set = emptySet(), +) : Task() { + private var screenHandler: H? = null + private var slotsLoaded = false + + init { + listener> { + screenHandler = it.screenHandler + + if (!waitForSlotLoad || slotsLoaded) success(it.screenHandler) + } + + listener> { + screenHandler = null + } + + listener { + slotsLoaded = true + + screenHandler?.let { + success(it) + } + } + + listener { event -> + if (screenHandler != null) return@listener + event.context = lookAtBlock(blockPos, rotationConfig, interactionConfig, sides) + } + + listener { + if (screenHandler != null) return@listener + if (!it.context.isValid) return@listener + val hitResult = it.context.hitResult?.blockResult ?: return@listener + interaction.interactBlock(player, Hand.MAIN_HAND, hitResult) + } + } + + companion object { + @Ta5kBuilder + inline fun openContainer( + blockPos: BlockPos, + waitForSlotLoad: Boolean = true + ) = OpenContainer(blockPos, waitForSlotLoad) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt b/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt new file mode 100644 index 000000000..80fb69945 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt @@ -0,0 +1,109 @@ +package com.lambda.task.tasks + +import com.lambda.Lambda.LOG +import com.lambda.context.SafeContext +import com.lambda.event.events.RotationEvent +import com.lambda.event.events.WorldEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.module.modules.client.TaskFlow +import com.lambda.task.Task +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Communication.info +import net.minecraft.block.BlockState + +class PlaceBlock @Ta5kBuilder constructor( + private val ctx: PlaceContext, + private val swingHand: Boolean, + private val rotate: Boolean, + private val waitForConfirmation: Boolean, +) : Task() { + private var beginState: BlockState? = null + override var cooldown = Int.MAX_VALUE + get() = maxOf(TaskFlow.build.placeCooldown, TaskFlow.taskCooldown) + override var timeout = 20 + private var placed = false + + private val SafeContext.resultingState: BlockState get() = + ctx.resultingPos.blockState(world) + private val SafeContext.matches get() = + ctx.targetState.matches(ctx.resultingPos.blockState(world), ctx.resultingPos, world) + + override fun SafeContext.onStart() { + if (matches) { + finish() + return + } + beginState = resultingState + + if (!rotate) placeBlock() + } + + init { + listener { event -> + if (placed) return@listener + if (!rotate) return@listener + event.context = ctx.rotation + } + + listener { + if (placed) return@listener + if (!rotate) return@listener + if (!it.context.isValid) return@listener + + placeBlock() + } + + listener { + if (it.pos != ctx.resultingPos) return@listener + + if (ctx.targetState.matches(it.state, it.pos, world)) { + finish() + } + } + } + + private fun SafeContext.placeBlock() { + val actionResult = interaction.interactBlock( + player, + ctx.hand, + ctx.result + ) + + if (actionResult.isAccepted) { + if (actionResult.shouldSwingHand() && swingHand) { + player.swingHand(ctx.hand) + } + + if (!player.getStackInHand(ctx.hand).isEmpty && interaction.hasCreativeInventory()) { + mc.gameRenderer.firstPersonRenderer.resetEquipProgress(ctx.hand) + } + + if (matches) { + placed = true + if (!waitForConfirmation) finish() + } + } else { + info("Internal interaction failed with $actionResult") + } + } + + private fun SafeContext.finish() { + LOG.info("Placed at ${ + ctx.result.blockPos.toShortString() + } (${ctx.result.side}) with expecting state ${ + ctx.expectedState + } and expecting position at ${ctx.resultingPos.toShortString()}") + success(Unit) + } + + companion object { + @Ta5kBuilder + fun placeBlock( + ctx: PlaceContext, + swingHand: Boolean = TaskFlow.interact.swingHand, + rotate: Boolean = TaskFlow.build.rotateForPlace, + waitForConfirmation: Boolean = TaskFlow.build.placeConfirmation, + ) = PlaceBlock(ctx, swingHand, rotate, waitForConfirmation) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt b/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt new file mode 100644 index 000000000..b0fd34c96 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt @@ -0,0 +1,53 @@ +package com.lambda.task.tasks + +import com.lambda.context.SafeContext +import com.lambda.interaction.construction.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.PlaceResult +import com.lambda.interaction.construction.result.Resolvable +import com.lambda.interaction.construction.simulation.BuildSimulator.simulate +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.task.Task +import com.lambda.task.tasks.BuildStructure.Companion.buildStructure +import com.lambda.util.BlockUtils.blockPos +import net.minecraft.item.ItemStack +import net.minecraft.util.math.BlockPos + +class PlaceContainer @Ta5kBuilder constructor( + val stack: ItemStack, +) : Task() { + override fun SafeContext.onStart() { + val results = BlockPos.iterateOutwards(player.blockPos, 4, 3, 4) + .map { it.blockPos } + .flatMap { + it.blockPos + .toStructure(TargetState.Stack(stack)) + .toBlueprint() + .simulate(player.getCameraPosVec(mc.tickDelta)) + } + +// val res = results.sorted() +// res + + val succeeds = results.filterIsInstance() + val wrongStacks = results.filterIsInstance() + (succeeds + wrongStacks).minOrNull()?.let { result -> + buildStructure { + result.blockPos + .toStructure(TargetState.Stack(stack)) + .toBlueprint() + }.onSuccess { _, _ -> + success(result.blockPos) + }.start(this@PlaceContainer) + } ?: { + failure("No valid placement found") + } + } + + companion object { + @Ta5kBuilder + fun placeContainer(stack: ItemStack) = + PlaceContainer(stack) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/tasks/TaskTester.kt b/common/src/main/kotlin/com/lambda/task/tasks/TaskTester.kt deleted file mode 100644 index 3cec22a1e..000000000 --- a/common/src/main/kotlin/com/lambda/task/tasks/TaskTester.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.lambda.task.tasks - -import com.lambda.Lambda.LOG -import com.lambda.event.events.KeyPressEvent -import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.threading.taskContext -import org.lwjgl.glfw.GLFW - -object TaskTester { - init { - listener { - if (it.keyCode != GLFW.GLFW_KEY_Z) { - return@listener - } - - taskContext { - HelloWorldTask() - .withDelay(500L) - .withTimeout(200L) - .withRepeats(10) - .withMaxAttempts(5) - .onSuccess { - LOG.info("Hello, World! Task completed") - }.onRepeat { repeats -> - LOG.info("Hello, World! Task $name repeated $repeats times") - }.onTimeout { - LOG.warn("Hello, World! Task $name timed out") - HelloWorldTask().execute() - }.onRetry { - LOG.warn("Hello, World! Task $name retrying") - }.onFailure { error -> - LOG.error("Hello, World! Task failed", error) - }.execute() - } - } - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt b/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt index b0051f4b7..fe09a5d5c 100644 --- a/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt +++ b/common/src/main/kotlin/com/lambda/threading/MainThreadInit.kt @@ -8,7 +8,7 @@ class MainThreadInit(private val initializer: () -> T) { operator fun getValue(thisRef: Any?, property: KProperty<*>) = value init { - runOnGameThread { + runGameScheduled { value = initializer() } } diff --git a/common/src/main/kotlin/com/lambda/threading/Threading.kt b/common/src/main/kotlin/com/lambda/threading/Threading.kt index 51fbbb3f2..403bb66bd 100644 --- a/common/src/main/kotlin/com/lambda/threading/Threading.kt +++ b/common/src/main/kotlin/com/lambda/threading/Threading.kt @@ -1,10 +1,13 @@ package com.lambda.threading -import com.lambda.Lambda +import com.lambda.Lambda.mc import com.lambda.context.ClientContext import com.lambda.context.SafeContext import com.lambda.event.EventFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await import kotlinx.coroutines.launch +import java.util.concurrent.CompletableFuture /** * Executes a block of code only if the context is safe. A context is considered safe when all the following properties are not null: @@ -18,13 +21,12 @@ import kotlinx.coroutines.launch * @param block The block of code to be executed within the safe context. * @return The result of the block execution if the context is safe, null otherwise. */ -inline fun runSafe(block: SafeContext.() -> T): T? { - return ClientContext().toSafe()?.let { block(it) } -} +inline fun runSafe(block: SafeContext.() -> T) = + ClientContext().toSafe()?.let { block(it) } /** * This function is used to execute a block of code on a new thread running asynchronously to the game thread. - * It should only be used when you need to perform read actions on the game data. + * It should only be used when you need to perform read actions on the game data (not write). * * Caution: Using this function to write to the game data can lead to race conditions. Therefore, it is recommended * to use this function only for read operations to avoid potential concurrency issues. @@ -36,11 +38,15 @@ inline fun runConcurrent(crossinline block: suspend () -> Unit) = block() } -inline fun taskContext(crossinline block: suspend () -> Unit) { +inline fun runIO(crossinline block: suspend () -> Unit) = + EventFlow.lambdaScope.launch(Dispatchers.IO) { + block() + } + +inline fun taskContext(crossinline block: suspend () -> Unit) = EventFlow.lambdaScope.launch { block() } -} /** * This function is used to execute a block of code within a safe context on a new thread running asynchronously to the game thread. @@ -72,8 +78,8 @@ inline fun runSafeConcurrent(crossinline block: SafeContext.() -> Unit) { * * @param block The task to be executed on the game's main thread. */ -inline fun runOnGameThread(crossinline block: () -> Unit) { - Lambda.mc.executeSync { block() } +inline fun runGameScheduled(crossinline block: () -> Unit) { + mc.executeSync { block() } } /** @@ -93,6 +99,27 @@ inline fun runOnGameThread(crossinline block: () -> Unit) { * * @param block The task to be executed on the game's main thread within a safe context. */ -inline fun runSafeOnGameThread(crossinline block: SafeContext.() -> Unit) { - runOnGameThread { runSafe { block() } } -} \ No newline at end of file +inline fun runSafeGameConcurrent(crossinline block: SafeContext.() -> Unit) { + runGameScheduled { runSafe { block() } } +} + +/** + * Executes a given task on the game's main thread within a safe context + * and blocks the coroutine until the task is completed. + * A context is considered safe when all the following properties are not null: + * - [SafeContext.world] + * - [SafeContext.player] + * - [SafeContext.interaction] + * - [SafeContext.connection] + * + * This function is used when a task needs to be performed on the game's main thread, + * as certain operations are not safe to perform on other threads. + * + * Note: + * This function is blocking + * as it uses [CompletableFuture]'s [await] method to [suspend] the coroutine until the task is completed. + * + * @param block The task to be executed on the game's main thread within a safe context. + */ +suspend inline fun runGameBlocking(noinline block: SafeContext.() -> T) = + CompletableFuture.supplyAsync({ runSafe { block() } }, mc).await() ?: throw IllegalStateException("Unsafe") \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/BaritoneUtils.kt b/common/src/main/kotlin/com/lambda/util/BaritoneUtils.kt index 694912e79..4701e33b7 100644 --- a/common/src/main/kotlin/com/lambda/util/BaritoneUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/BaritoneUtils.kt @@ -3,6 +3,7 @@ package com.lambda.util import baritone.api.BaritoneAPI import baritone.api.IBaritone import baritone.api.Settings +import baritone.api.pathing.goals.Goal object BaritoneUtils { private val baritone = BaritoneAPI.getProvider() @@ -17,5 +18,7 @@ object BaritoneUtils { get() = primary.customGoalProcess.isActive || primary.pathingBehavior.isPathing || primary.pathingControlManager.mostRecentInControl() .orElse(null)?.isActive == true + fun setGoalAndPath(goal: Goal) = primary.customGoalProcess.setGoalAndPath(goal) + fun cancel() = primary.pathingBehavior.cancelEverything() } diff --git a/common/src/main/kotlin/com/lambda/util/BlockUtils.kt b/common/src/main/kotlin/com/lambda/util/BlockUtils.kt new file mode 100644 index 000000000..581e60c7a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/BlockUtils.kt @@ -0,0 +1,113 @@ +package com.lambda.util + +import com.lambda.context.SafeContext +import com.lambda.util.item.ItemUtils.block +import com.lambda.util.item.ItemUtils.shulkerBoxes +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.client.world.ClientWorld +import net.minecraft.fluid.FluidState +import net.minecraft.fluid.Fluids +import net.minecraft.item.Item +import net.minecraft.util.math.* +import kotlin.math.floor + +object BlockUtils { + private val shulkerBlocks = shulkerBoxes.map { it.block } + + val interactionBlacklist = mutableSetOf( + Blocks.CHEST, + Blocks.TRAPPED_CHEST, + Blocks.ENDER_CHEST, + Blocks.BARREL, + Blocks.REPEATER, + Blocks.COMPARATOR, + Blocks.DISPENSER, + Blocks.DROPPER, + Blocks.HOPPER, + Blocks.BREWING_STAND, + Blocks.FURNACE, + Blocks.BLAST_FURNACE, + Blocks.SMOKER, + Blocks.CRAFTING_TABLE, + Blocks.ANVIL, + Blocks.CHIPPED_ANVIL, + Blocks.DAMAGED_ANVIL, + Blocks.ENCHANTING_TABLE, + Blocks.BEACON, + Blocks.BELL, + Blocks.CAMPFIRE, + Blocks.SOUL_CAMPFIRE, + Blocks.JUKEBOX, + Blocks.NOTE_BLOCK, + ).apply { addAll(shulkerBlocks) } + + val signs = setOf( + Blocks.OAK_SIGN, + Blocks.BIRCH_SIGN, + Blocks.ACACIA_SIGN, + Blocks.CHERRY_SIGN, + Blocks.JUNGLE_SIGN, + Blocks.DARK_OAK_SIGN, + Blocks.MANGROVE_SIGN, + Blocks.BAMBOO_SIGN, + ) + + val wallSigns = setOf( + Blocks.OAK_WALL_SIGN, + Blocks.BIRCH_WALL_SIGN, + Blocks.ACACIA_WALL_SIGN, + Blocks.CHERRY_WALL_SIGN, + Blocks.JUNGLE_WALL_SIGN, + Blocks.DARK_OAK_WALL_SIGN, + Blocks.MANGROVE_WALL_SIGN, + Blocks.BAMBOO_WALL_SIGN, + ) + + val hangingSigns = setOf( + Blocks.OAK_HANGING_SIGN, + Blocks.BIRCH_HANGING_SIGN, + Blocks.ACACIA_HANGING_SIGN, + Blocks.CHERRY_HANGING_SIGN, + Blocks.JUNGLE_HANGING_SIGN, + Blocks.DARK_OAK_HANGING_SIGN, + Blocks.MANGROVE_HANGING_SIGN, + Blocks.BAMBOO_HANGING_SIGN, + ) + + val hangingWallSigns = setOf( + Blocks.OAK_WALL_HANGING_SIGN, + Blocks.BIRCH_WALL_HANGING_SIGN, + Blocks.ACACIA_WALL_HANGING_SIGN, + Blocks.CHERRY_WALL_HANGING_SIGN, + Blocks.JUNGLE_WALL_HANGING_SIGN, + Blocks.DARK_OAK_WALL_HANGING_SIGN, + Blocks.MANGROVE_WALL_HANGING_SIGN, + Blocks.BAMBOO_WALL_HANGING_SIGN, + ) + + val fluids = listOf( + Fluids.LAVA, + Fluids.FLOWING_LAVA, + Fluids.WATER, + Fluids.FLOWING_WATER, + Fluids.EMPTY, + ) + + val allSigns = signs + wallSigns + hangingSigns + hangingWallSigns + + fun BlockPos.blockState(world: ClientWorld): BlockState = world.getBlockState(this) + fun BlockPos.fluidState(world: ClientWorld): FluidState = world.getFluidState(this) + fun BlockPos.blockEntity(world: ClientWorld) = world.getBlockEntity(this) + fun SafeContext.instantBreakable(blockState: BlockState, blockPos: BlockPos): Boolean { + val ticksNeeded = 1 / blockState.calcBlockBreakingDelta(player, world, blockPos) + return (ticksNeeded <= 1 && ticksNeeded != 0f) || player.isCreative + } + val Vec3i.blockPos: BlockPos get() = BlockPos(this) + val Block.item: Item get() = asItem() + val Vec3d.flooredPos: BlockPos get() = BlockPos(floor(x).toInt(), floor(y).toInt(), floor(z).toInt()) + fun BlockPos.vecOf(direction: Direction): Vec3d = toCenterPos().add(Vec3d.of(direction.vector).multiply(0.5)) + fun BlockPos.offset(eightWayDirection: EightWayDirection, amount: Int): BlockPos = + add(eightWayDirection.offsetX * amount, 0, eightWayDirection.offsetZ * amount) +} diff --git a/common/src/main/kotlin/com/lambda/util/Communication.kt b/common/src/main/kotlin/com/lambda/util/Communication.kt index fad072906..868cddbc4 100644 --- a/common/src/main/kotlin/com/lambda/util/Communication.kt +++ b/common/src/main/kotlin/com/lambda/util/Communication.kt @@ -10,7 +10,7 @@ import com.lambda.module.Module import com.lambda.module.ModuleRegistry import com.lambda.module.modules.client.GuiSettings import com.lambda.threading.runSafe -import com.lambda.threading.runSafeOnGameThread +import com.lambda.threading.runSafeGameConcurrent import com.lambda.util.StringUtils.capitalize import com.lambda.util.text.* import net.minecraft.client.toast.SystemToast @@ -45,7 +45,7 @@ object Communication { buildText { text(this@toast.source(logLevel, color = Color.YELLOW)) }.let { title -> - runSafeOnGameThread { + runSafeGameConcurrent { mc.toastManager.add(logLevel.toast(title, message)) } } @@ -70,7 +70,7 @@ object Communication { text(this@log.source(logLevel, source, textSource)) text(message) }.let { log -> - runSafeOnGameThread { + runSafeGameConcurrent { player.sendMessage(log) } } diff --git a/common/src/main/kotlin/com/lambda/util/DebugInfoHud.kt b/common/src/main/kotlin/com/lambda/util/DebugInfoHud.kt index e5c3360a6..c735a7b4f 100644 --- a/common/src/main/kotlin/com/lambda/util/DebugInfoHud.kt +++ b/common/src/main/kotlin/com/lambda/util/DebugInfoHud.kt @@ -5,6 +5,7 @@ import com.lambda.Lambda.mc import com.lambda.command.CommandRegistry import com.lambda.event.EventFlow import com.lambda.module.ModuleRegistry +import com.lambda.util.Formatting.asString import net.minecraft.util.Formatting import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.EntityHitResult @@ -36,6 +37,8 @@ object DebugInfoHud { null -> add("Crosshair Target: None") } + add("Eye Pos: ${mc.cameraEntity?.getCameraPosVec(mc.tickDelta)?.asString(3)}") + return } } diff --git a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt index c2132c11a..230241523 100644 --- a/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt +++ b/common/src/main/kotlin/com/lambda/util/DynamicReflectionSerializer.kt @@ -1,5 +1,6 @@ package com.lambda.util +import com.lambda.util.DynamicReflectionSerializer.formatFieldValue import com.mojang.serialization.Codec import net.minecraft.block.BlockState import net.minecraft.client.resource.language.TranslationStorage @@ -86,7 +87,7 @@ object DynamicReflectionSerializer { } val fieldValue = field.get(this) val fieldIndent = indent + " ".repeat(INDENT) - builder.appendLine("$fieldIndent${field.name}: ${formatFieldValue(fieldValue)}") + builder.appendLine("$fieldIndent${field.name}: ${fieldValue.formatFieldValue()}") if (currentDepth < maxRecursionDepth && fieldValue != null @@ -105,22 +106,20 @@ object DynamicReflectionSerializer { } } - private fun formatFieldValue(value: Any?): String { - return when (value) { - is String -> "\"$value\"" - is Collection<*> -> "[${value.joinToString(", ") { formatFieldValue(it) }}]" - is Array<*> -> "[${value.joinToString(", ") { formatFieldValue(it) }}]" + private fun Any?.formatFieldValue(): String = + when (this) { + is String -> "\"${this}\"" + is Collection<*> -> "[${joinToString(", ") { it.formatFieldValue() }}]" + is Array<*> -> "[${joinToString(", ") { it.formatFieldValue() }}]" is Map<*, *> -> "{${ - value.entries.joinToString(", ") { (k, v) -> - "${formatFieldValue(k)}: ${formatFieldValue(v)}" + entries.joinToString(", ") { (k, v) -> + "${k.formatFieldValue()}: ${v.formatFieldValue()}" } }}" - - is Text -> value.string - is Identifier -> "${value.namespace}:${value.path}" - is NbtCompound -> value.asString() - is RegistryEntry<*> -> "${value.value()}" - else -> value?.toString() ?: "null" + is Text -> string + is Identifier -> "$namespace:$path" + is NbtCompound -> asString() + is RegistryEntry<*> -> "${value()}" + else -> this?.toString() ?: "null" } - } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/Formatting.kt b/common/src/main/kotlin/com/lambda/util/Formatting.kt index e972bf4e3..5c597c39f 100644 --- a/common/src/main/kotlin/com/lambda/util/Formatting.kt +++ b/common/src/main/kotlin/com/lambda/util/Formatting.kt @@ -7,7 +7,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter object Formatting { - val Vec3d.asString: String + val Vec3d.string: String get() = asString() fun Vec3d.asString(decimals: Int = 2): String { diff --git a/common/src/main/kotlin/com/lambda/util/collections/Cacheable.kt b/common/src/main/kotlin/com/lambda/util/collections/Cacheable.kt new file mode 100644 index 000000000..8f68e40cb --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/collections/Cacheable.kt @@ -0,0 +1,14 @@ +package com.lambda.util.collections + +import kotlin.reflect.KProperty + +class Cacheable private constructor(private val getter: (K) -> V) { + private val cache = mutableMapOf() + + operator fun getValue(thisRef: K, property: KProperty<*>) = + cache.getOrPut(thisRef) { getter(thisRef) } + + companion object { + fun cacheable(getter: (K) -> V) = Cacheable(getter) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/combat/Explosion.kt b/common/src/main/kotlin/com/lambda/util/combat/Explosion.kt index 2d644a23c..ac559c39c 100644 --- a/common/src/main/kotlin/com/lambda/util/combat/Explosion.kt +++ b/common/src/main/kotlin/com/lambda/util/combat/Explosion.kt @@ -1,6 +1,8 @@ package com.lambda.util.combat import com.lambda.context.SafeContext +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.fluidState import com.lambda.util.math.VecUtils.minus import com.lambda.util.math.VecUtils.times import com.lambda.util.world.WorldUtils.getFastEntities @@ -104,8 +106,8 @@ object Explosion { while (intensity > 0) { val blockPos = BlockPos.ofFloored(explosionX, explosionY, explosionZ) - val block = world.getBlockState(blockPos) - val fluid = world.getFluidState(blockPos) + val block = blockPos.blockState(world) + val fluid = blockPos.fluidState(world) if (!world.isInBuildLimit(blockPos)) { break } diff --git a/common/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt b/common/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt new file mode 100644 index 000000000..aa15c663a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt @@ -0,0 +1,64 @@ +package com.lambda.util.item + +import com.lambda.util.collections.Cacheable.Companion.cacheable +import net.minecraft.inventory.Inventories +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtElement +import net.minecraft.util.collection.DefaultedList + +object ItemStackUtils { + val ItemStack.spaceLeft get() = maxCount - count + val ItemStack.hasSpace get() = spaceLeft > 0 + val List.spaceLeft get() = sumOf { it.spaceLeft } + val List.empty: Int get() = count { it.isEmpty } + val List.count: Int get() = sumOf { it.count } + val List.copy: List get() = map { it.copy() } + + val List.compressed: List get() = + fold(mutableListOf()) { acc, itemStack -> + acc merge itemStack + acc + } + + infix fun List.merge(other: ItemStack): List { + return flatMap { + it merge other + } + } + + infix fun ItemStack.merge(other: ItemStack): List { + if (!isStackable || !other.isStackable) { + return listOf(this, other) + } + + val newCount = count + other.count + if (newCount <= maxCount) { + return listOf(copyWithCount(newCount)) + } + val remainder = newCount - maxCount + return listOf(copyWithCount(maxCount), copyWithCount(remainder)) + } + + val ItemStack.shulkerBoxContents: List by cacheable { stack -> + BlockItem.getBlockEntityNbt(stack)?.takeIf { + it.contains("Items", NbtElement.LIST_TYPE.toInt()) + }?.let { + val list = DefaultedList.ofSize(27, ItemStack.EMPTY) + Inventories.readNbt(it, list) + list + } ?: emptyList() + } + + /** + * Checks if the given item stacks are equal, including the item count and NBT. + * + * @param other The other item stack to compare with. + * @return `true` if the item stacks are equal, `false` otherwise. + * @see ItemStack.areItemsEqual Checks if the items in two item stacks are equal. + * @see ItemStack.canCombine Checks if two item stacks can be combined into one stack. + */ + fun ItemStack?.equal(other: ItemStack?) = ItemStack.areEqual(this, other) + + fun ItemStack.combines(other: ItemStack) = ItemStack.canCombine(this, other) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt b/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt new file mode 100644 index 000000000..3d9455762 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt @@ -0,0 +1,142 @@ +package com.lambda.util.item + +import net.minecraft.block.Block +import net.minecraft.item.Item +import net.minecraft.item.Items + +object ItemUtils { + val pickaxes = setOf( + Items.WOODEN_PICKAXE, + Items.STONE_PICKAXE, + Items.IRON_PICKAXE, + Items.GOLDEN_PICKAXE, + Items.DIAMOND_PICKAXE, + Items.NETHERITE_PICKAXE, + ) + + val shovels = setOf( + Items.WOODEN_SHOVEL, + Items.STONE_SHOVEL, + Items.IRON_SHOVEL, + Items.GOLDEN_SHOVEL, + Items.DIAMOND_SHOVEL, + Items.NETHERITE_SHOVEL, + ) + + val axes = setOf( + Items.WOODEN_AXE, + Items.STONE_AXE, + Items.IRON_AXE, + Items.GOLDEN_AXE, + Items.DIAMOND_AXE, + Items.NETHERITE_AXE, + ) + + val hoes = setOf( + Items.WOODEN_HOE, + Items.STONE_HOE, + Items.IRON_HOE, + Items.GOLDEN_HOE, + Items.DIAMOND_HOE, + Items.NETHERITE_HOE, + ) + + val swords = setOf( + Items.WOODEN_SWORD, + Items.STONE_SWORD, + Items.IRON_SWORD, + Items.GOLDEN_SWORD, + Items.DIAMOND_SWORD, + Items.NETHERITE_SWORD, + ) + + val misc = setOf( + Items.SHEARS, + ) + + val tools = pickaxes + shovels + axes + hoes + swords + misc + + val shulkerBoxes = setOf( + Items.SHULKER_BOX, + Items.WHITE_SHULKER_BOX, + Items.ORANGE_SHULKER_BOX, + Items.MAGENTA_SHULKER_BOX, + Items.LIGHT_BLUE_SHULKER_BOX, + Items.YELLOW_SHULKER_BOX, + Items.LIME_SHULKER_BOX, + Items.PINK_SHULKER_BOX, + Items.GRAY_SHULKER_BOX, + Items.LIGHT_GRAY_SHULKER_BOX, + Items.CYAN_SHULKER_BOX, + Items.PURPLE_SHULKER_BOX, + Items.BLUE_SHULKER_BOX, + Items.BROWN_SHULKER_BOX, + Items.GREEN_SHULKER_BOX, + Items.RED_SHULKER_BOX, + Items.BLACK_SHULKER_BOX, + ) + + val chests = setOf( + Items.CHEST, + Items.TRAPPED_CHEST, + Items.ENDER_CHEST, + Items.BARREL, + ) + + val defaultDisposables = setOf( + Items.DIRT, + Items.COBBLESTONE, + Items.GRANITE, + Items.DIORITE, + Items.ANDESITE, + Items.SANDSTONE, + Items.RED_SANDSTONE, + Items.NETHERRACK, + Items.END_STONE, + Items.STONE, + Items.BASALT, + Items.BLACKSTONE, + Items.COBBLED_DEEPSLATE + ) + + val Item.block: Block get() = Block.getBlockFromItem(this) + + fun Int.toItemCount(): String { + if (this < 0) { + return "Invalid input" + } + + return buildString { + val dubs = this@toItemCount / (54 * 27) + val shulkers = (this@toItemCount % (54 * 27)) / 64 + val remainingItems = this@toItemCount % 64 + + if (dubs > 0) { + append("$dubs dub") + if (dubs > 1) { + append("s") + } + if (shulkers > 0 || remainingItems > 0) { + append(" ") + } + } + + if (shulkers > 0) { + append("$shulkers shulker") + if (shulkers > 1) { + append("s") + } + if (remainingItems > 0) { + append(" ") + } + } + + if (remainingItems > 0) { + append("$remainingItems item") + if (remainingItems > 1) { + append("s") + } + } + } + } +} diff --git a/common/src/main/kotlin/com/lambda/util/player/MovementUtils.kt b/common/src/main/kotlin/com/lambda/util/player/MovementUtils.kt index a56e4adab..45550025d 100644 --- a/common/src/main/kotlin/com/lambda/util/player/MovementUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/player/MovementUtils.kt @@ -8,6 +8,7 @@ import com.lambda.util.math.MathUtils.toRadian import net.minecraft.client.input.Input import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.entity.Entity +import net.minecraft.util.math.EightWayDirection import net.minecraft.util.math.Vec3d import kotlin.math.cos import kotlin.math.hypot @@ -84,4 +85,17 @@ object MovementUtils { val Entity.moveDiff get() = Vec3d(this.pos.x - this.prevX, this.pos.y - this.prevY, this.pos.z - this.prevZ) val Entity.moveDelta get() = moveDiff.let { hypot(it.x, it.z) } val Entity.motionDelta get() = hypot(this.velocity.x, this.velocity.z) + + val Entity.octant: EightWayDirection + get() { + // Normalize the yaw to be within the range of -180 to 179 degrees + var normalizedYaw = (yaw + 180.0) % 360.0 + if (normalizedYaw < 0) { + normalizedYaw += 360.0 + } + + // Calculate the index of the closest direction + val directionIndex = ((normalizedYaw + 22.5) / 45.0).toInt() % 8 + return EightWayDirection.entries[directionIndex] + } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/player/SlotUtils.kt b/common/src/main/kotlin/com/lambda/util/player/SlotUtils.kt new file mode 100644 index 000000000..ce1a94dd7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/player/SlotUtils.kt @@ -0,0 +1,30 @@ +package com.lambda.util.player + +import com.lambda.context.SafeContext +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.SlotActionType + +object SlotUtils { + val ClientPlayerEntity.hotbar: List get() = inventory.main.subList(0, 9) + val ClientPlayerEntity.storage: List get() = inventory.main.subList(9, 36) + val ClientPlayerEntity.hotbarAndStorage: List get() = inventory.main.subList(0, 36) + val ClientPlayerEntity.combined: List get() = inventory.main + inventory.armor + inventory.offHand + val ClientPlayerEntity.offhand: ItemStack get() = offHandStack + + fun SafeContext.clickSlot( + slotId: Int, + button: Int, + actionType: SlotActionType, + ) { + val syncId = player.currentScreenHandler?.syncId ?: return + + interaction.clickSlot( + syncId, + slotId, + button, + actionType, + player, + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/primitives/extension/Aliases.kt b/common/src/main/kotlin/com/lambda/util/primitives/extension/Aliases.kt index 5d00a898e..d59ea9904 100644 --- a/common/src/main/kotlin/com/lambda/util/primitives/extension/Aliases.kt +++ b/common/src/main/kotlin/com/lambda/util/primitives/extension/Aliases.kt @@ -1,6 +1,9 @@ package com.lambda.util.primitives.extension +import com.lambda.interaction.construction.verify.TargetState import com.mojang.brigadier.builder.LiteralArgumentBuilder import net.minecraft.command.CommandSource +import net.minecraft.util.math.BlockPos -typealias CommandBuilder = LiteralArgumentBuilder \ No newline at end of file +typealias CommandBuilder = LiteralArgumentBuilder +typealias Structure = Map \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/primitives/extension/Screen.kt b/common/src/main/kotlin/com/lambda/util/primitives/extension/Screen.kt new file mode 100644 index 000000000..35e7b56aa --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/primitives/extension/Screen.kt @@ -0,0 +1,12 @@ +package com.lambda.util.primitives.extension + +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot + +val ScreenHandler.containerSlots: List get() = slots.filter { it.inventory !is PlayerInventory } +val ScreenHandler.inventorySlots: List get() = slots.filter { it.inventory is PlayerInventory } + +val ScreenHandler.containerStacks: List get() = containerSlots.map { it.stack } +val ScreenHandler.inventoryStacks: List get() = inventorySlots.map { it.stack } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/util/world/StructureUtils.kt b/common/src/main/kotlin/com/lambda/util/world/StructureUtils.kt new file mode 100644 index 000000000..02c1a43a8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/world/StructureUtils.kt @@ -0,0 +1,65 @@ +package com.lambda.util.world + +import net.minecraft.block.Blocks +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.EightWayDirection + +object StructureUtils { + val PORTAL = setOf( + BlockPos(0, 0, 0), + BlockPos(0, 1, 0), + BlockPos(0, 2, 0), + BlockPos(0, 3, 0), + BlockPos(1, 3, 0), + BlockPos(2, 3, 0), + BlockPos(3, 3, 0), + BlockPos(1, -1, 0), + BlockPos(2, -1, 0), + BlockPos(3, 0, 0), + BlockPos(3, 1, 0), + BlockPos(3, 2, 0), + BlockPos(3, 3, 0), + ) + val LIGHT_UP = mapOf( + BlockPos(1, 0, 0) to Blocks.FIRE.defaultState, + ) + + /** + * Generates a tube of blocks in the specified direction. + * + * @param direction The direction in which the tube should be generated. + * @param width The width of the tube. + * @param height The height of the tube. + * @param leftRightOffset The offset along the X-axis. + * @param heightOffset The offset along the Y-axis. + * @return A set of BlockPos representing the generated tube. + */ + fun generateDirectionalTube( + direction: EightWayDirection, + width: Int, + height: Int, + leftRightOffset: Int, + heightOffset: Int, + ): Set { + val tube = mutableSetOf() + + val offsetX = leftRightOffset * direction.offsetX + val offsetZ = leftRightOffset * direction.offsetZ + + (heightOffset until heightOffset + height).forEach { y -> + (0 until width).forEach { x -> + (0 until width).forEach { z -> + tube.add( + BlockPos( + offsetX + x * direction.offsetX, + y, + offsetZ + z * direction.offsetZ, + ), + ) + } + } + } + + return tube + } +} diff --git a/common/src/main/kotlin/com/lambda/util/world/WorldUtils.kt b/common/src/main/kotlin/com/lambda/util/world/WorldUtils.kt index b0a322e43..7ad0eabef 100644 --- a/common/src/main/kotlin/com/lambda/util/world/WorldUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/world/WorldUtils.kt @@ -1,6 +1,8 @@ package com.lambda.util.world import com.lambda.context.SafeContext +import com.lambda.util.BlockUtils.blockPos +import com.lambda.util.BlockUtils.blockState import com.lambda.util.collections.filterPointer import net.minecraft.block.Block import net.minecraft.block.BlockState @@ -120,8 +122,10 @@ object WorldUtils { for (x in sectionX - chunks..sectionX + chunks) { for (y in sectionY - chunks..sectionY + chunks) { for (z in sectionZ - chunks..sectionZ + chunks) { - val section = - world.entityManager.cache.findTrackingSection(ChunkSectionPos.asLong(x, y, z)) ?: continue + val section = world + .entityManager + .cache + .findTrackingSection(ChunkSectionPos.asLong(x, y, z)) ?: continue section.collection.filterPointer(pointer, iterator) { entity -> entity != player && @@ -166,8 +170,8 @@ object WorldUtils { * @param rangeY The maximum distance to search for entities in the y-axis. * @param rangeZ The maximum distance to search for entities in the z-axis. * @param pointer The mutable list to store the positions in. - * @param iterator Iterator to perform operations on each block. * @param predicate Predicate to filter the blocks. + * @param iterator Iterator to perform operations on each block. */ inline fun SafeContext.searchBlocks( pos: Vec3i, @@ -175,9 +179,9 @@ object WorldUtils { rangeY: Int, rangeZ: Int, pointer: MutableList? = null, - iterator: (BlockState, BlockPos, Int) -> Unit = { _, _, _ -> }, predicate: (BlockState, BlockPos) -> Boolean = { _, _ -> true }, - ) = searchBlocks(pos, Vec3i(rangeX, rangeY, rangeZ), pointer, iterator, predicate) + iterator: (BlockState, BlockPos, Int) -> Unit = { _, _, _ -> }, + ) = searchBlocks(pos, Vec3i(rangeX, rangeY, rangeZ), pointer, predicate, iterator) /** * Returns all the position within the range where the predicate is true. @@ -192,11 +196,11 @@ object WorldUtils { pos: Vec3i, range: Vec3i, pointer: MutableList? = null, - iterator: (BlockState, BlockPos, Int) -> Unit = { _, _, _ -> }, predicate: (BlockState, BlockPos) -> Boolean = { _, _ -> true }, + iterator: (BlockState, BlockPos, Int) -> Unit = { _, _, _ -> }, ) { iteratePositions(pos, range) { blockPos, index -> - val state = world.getBlockState(blockPos) + val state = blockPos.blockState(world) if (predicate(state, blockPos)) { pointer?.add(state.block) iterator(state, blockPos, index) @@ -209,21 +213,21 @@ object WorldUtils { * * @param pos The position to search from. * @param range The maximum distance to search for fluids in each axis. - * @param pointer The mutable list to store the positions in. + * @param collector The mutable list to store the positions in. * @param iterator Iterator to perform operations on each fluid. * @param predicate Predicate to filter the fluids. */ inline fun SafeContext.searchFluids( pos: Vec3i, range: Vec3i, - pointer: MutableList? = null, + collector: MutableList? = null, iterator: (FluidState, BlockPos, Int) -> Unit = { _, _, _ -> }, predicate: (FluidState, BlockPos) -> Boolean = { _, _ -> true }, ) { iteratePositions(pos, range) { blockPos, index -> val state = world.getFluidState(blockPos) if (predicate(state, blockPos)) { - pointer?.add(state.fluid as? T ?: return@iteratePositions) + collector?.add(state.fluid as? T ?: return@iteratePositions) iterator(state, blockPos, index) } } @@ -235,12 +239,12 @@ object WorldUtils { * @param range The maximum distance to search for entities in each axis. * @param iterator Iterator to perform operations on each position. */ - inline fun SafeContext.iteratePositions( + inline fun iteratePositions( pos: Vec3i, range: Vec3i, iterator: (BlockPos, Int) -> Unit, ) { - BlockPos.iterateOutwards(BlockPos(pos), range.x, range.y, range.z) + BlockPos.iterateOutwards(pos.blockPos, range.x, range.y, range.z) .forEachIndexed { index, blockPos -> iterator(blockPos, index) } diff --git a/common/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt b/common/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt index 4d4f10d3b..54c8746e1 100644 --- a/common/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/world/raycast/RayCastUtils.kt @@ -53,7 +53,7 @@ object RayCastUtils { // ToDo: Should rather move player hitbox down and check collision fun distanceToGround(maxDist: Double = 100.0) = runSafe { val pos = player.pos.add(0.0, 0.1, 0.0) - val cast = Rotation.DOWN.rayCast(maxDist, RayCastMask.BLOCK, pos, false) ?: return@runSafe maxDist + val cast = Rotation.DOWN.rayCast(maxDist, pos, false, RayCastMask.BLOCK) ?: return@runSafe maxDist return@runSafe max(0.0, pos.y - cast.pos.y) } @@ -70,6 +70,8 @@ object RayCastUtils { return this as? BlockHitResult } + fun HitResult.distanceTo(pos: Vec3d) = this.pos.distanceTo(pos) + val HitResult.orNull get() = entityResult ?: blockResult val HitResult?.orMiss diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index 2c5745564..79683133f 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -4,9 +4,12 @@ accessible field net/minecraft/client/MinecraftClient itemUseCooldown I accessible field net/minecraft/client/MinecraftClient attackCooldown I accessible field net/minecraft/client/MinecraftClient paused Z accessible field net/minecraft/client/MinecraftClient pausedTickDelta F +accessible field net/minecraft/client/MinecraftClient thread Ljava/lang/Thread; # World accessible field net/minecraft/client/world/ClientWorld entityManager Lnet/minecraft/client/world/ClientEntityManager; +accessible field net/minecraft/client/world/ClientEntityManager cache Lnet/minecraft/world/entity/SectionedEntityCache; +accessible field net/minecraft/world/entity/EntityTrackingSection collection Lnet/minecraft/util/collection/TypeFilterableList; # Entity accessible field net/minecraft/entity/projectile/FireworkRocketEntity shooter Lnet/minecraft/entity/LivingEntity; @@ -43,6 +46,7 @@ accessible class net/minecraft/network/packet/c2s/play/PlayerInteractEntityC2SPa accessible class net/minecraft/network/packet/c2s/play/PlayerInteractEntityC2SPacket$InteractAtHandler # Other -accessible field net/minecraft/client/world/ClientEntityManager cache Lnet/minecraft/world/entity/SectionedEntityCache; -accessible field net/minecraft/world/entity/EntityTrackingSection collection Lnet/minecraft/util/collection/TypeFilterableList; accessible field net/minecraft/world/explosion/Explosion behavior Lnet/minecraft/world/explosion/ExplosionBehavior; +accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List; +accessible method net/minecraft/item/BlockItem getPlacementState (Lnet/minecraft/item/ItemPlacementContext;)Lnet/minecraft/block/BlockState; +accessible field net/minecraft/client/network/ClientPlayerInteractionManager blockBreakingCooldown I \ No newline at end of file diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index d6781df13..bb1c37b3a 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -9,6 +9,7 @@ "baritone.MixinBaritonePlayerContext", "baritone.MixinLookBehavior", "entity.ClientPlayerEntityMixin", + "entity.ClientPlayInteractionManagerMixin", "entity.EntityMixin", "entity.LivingEntityMixin", "entity.PlayerEntityMixin", @@ -30,6 +31,7 @@ "render.LivingEntityRendererMixin", "render.RenderLayersMixin", "render.RenderTickCounterMixin", + "render.ScreenHandlerMixin", "render.VertexBufferMixin", "render.WorldRendererMixin", "world.ClientChunkManagerMixin", diff --git a/common/src/test/kotlin/TaskTests.kt b/common/src/test/kotlin/TaskTests.kt new file mode 100644 index 000000000..e47fa80b3 --- /dev/null +++ b/common/src/test/kotlin/TaskTests.kt @@ -0,0 +1,14 @@ +import com.lambda.task.Task.Companion.emptyTask +import kotlin.test.assertEquals + +internal class TaskTests { + fun testSum() { + val task = emptyTask("1").onSuccess { task, unit -> + emptyTask("2").onSuccess { task, unit -> + emptyTask("3").onSuccess { task, unit -> + emptyTask("4") + } + } + } + } +} \ No newline at end of file