diff --git a/build.gradle.kts b/build.gradle.kts index 576788c9c..f427633a0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + import org.gradle.internal.jvm.* import net.fabricmc.loom.api.LoomGradleExtensionAPI import org.apache.tools.ant.taskdefs.condition.Os diff --git a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 7f9dd2aa8..ebc46893d 100644 --- a/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/common/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -20,7 +20,7 @@ import com.lambda.Lambda; import com.lambda.event.EventFlow; import com.lambda.event.events.ClientEvent; -import com.lambda.event.events.ScreenHandlerEvent; +import com.lambda.event.events.InventoryEvent; import com.lambda.event.events.TickEvent; import com.lambda.module.modules.player.Interact; import net.minecraft.client.MinecraftClient; @@ -78,7 +78,7 @@ private void onStartup(CallbackInfo ci) { 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 InventoryEvent.Open(handledScreen.getScreenHandler())); } } @@ -86,7 +86,7 @@ private void onScreenOpen(@Nullable Screen screen, CallbackInfo ci) { 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 InventoryEvent.Close(handledScreen.getScreenHandler())); } } diff --git a/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java b/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java index 3f863fd52..43896514f 100644 --- a/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java +++ b/common/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java @@ -66,9 +66,11 @@ public void interactItemHead(PlayerEntity player, Hand hand, CallbackInfoReturna } } - @Inject(method = "attackBlock", at = @At("HEAD")) + @Inject(method = "attackBlock", at = @At("HEAD"), cancellable = true) public void onAttackBlock(BlockPos pos, Direction side, CallbackInfoReturnable cir) { - if (EventFlow.post(new PlayerEvent.Attack.Block(pos, side)).isCanceled()) cir.cancel(); + if (EventFlow.post(new PlayerEvent.Attack.Block(pos, side)).isCanceled()) { + cir.setReturnValue(false); + } } @Inject(method = "attackEntity", at = @At("HEAD"), cancellable = true) diff --git a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 000000000..e756a9f96 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.network; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.InventoryEvent; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public class ClientPlayNetworkHandlerMixin { + @Inject(method = "onUpdateSelectedSlot", at = @At(value = "TAIL")) + private void onUpdateSelectedSlot(UpdateSelectedSlotS2CPacket packet, CallbackInfo ci) { + EventFlow.post(new InventoryEvent.SelectedHotbarSlotUpdate(packet.getSlot())); + } + + @Inject(method = "onScreenHandlerSlotUpdate", at = @At(value = "TAIL")) + private void onScreenHandlerSlotUpdate(ScreenHandlerSlotUpdateS2CPacket packet, CallbackInfo ci) { + EventFlow.post(new InventoryEvent.SlotUpdate(packet.getSyncId(), packet.getRevision(), packet.getSlot(), packet.getStack())); + } +} 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 15d19aac3..c6027a33d 100644 --- a/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/DebugHudMixin.java @@ -17,7 +17,7 @@ package com.lambda.mixin.render; -import com.lambda.task.RootTask; +import com.lambda.task.TaskFlow; import com.lambda.util.DebugInfoHud; import net.minecraft.client.gui.hud.DebugHud; import org.spongepowered.asm.mixin.Mixin; @@ -36,6 +36,6 @@ private void onGetRightText(CallbackInfoReturnable> cir) { @Inject(method = "getLeftText", at = @At(value = "TAIL")) private void onGetLeftText(CallbackInfoReturnable> cir) { - RootTask.INSTANCE.addInfo(cir.getReturnValue()); + cir.getReturnValue().addAll(List.of(TaskFlow.INSTANCE.toString().split("\n"))); } } diff --git a/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java b/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java index 803825bf5..42ce6dd53 100644 --- a/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java +++ b/common/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java @@ -18,7 +18,7 @@ package com.lambda.mixin.render; import com.lambda.event.EventFlow; -import com.lambda.event.events.ScreenHandlerEvent; +import com.lambda.event.events.InventoryEvent; import net.minecraft.item.ItemStack; import net.minecraft.screen.ScreenHandler; import org.spongepowered.asm.mixin.Mixin; @@ -32,6 +32,6 @@ public class ScreenHandlerMixin { @Inject(method = "updateSlotStacks", at = @At("TAIL")) private void onUpdateSlotStacksHead(int revision, List stacks, ItemStack cursorStack, CallbackInfo ci) { - EventFlow.post(new ScreenHandlerEvent.Update(revision, stacks, cursorStack)); + EventFlow.post(new InventoryEvent.FullUpdate(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 40ecde9d4..6ca55a063 100644 --- a/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java +++ b/common/src/main/java/com/lambda/mixin/world/ClientWorldMixin.java @@ -34,13 +34,6 @@ @Mixin(ClientWorld.class) public class ClientWorldMixin { - @Inject(method = "handleBlockUpdate", at = @At("HEAD"), cancellable = true) - private void handleBlockUpdateInject(BlockPos pos, BlockState state, int flags, CallbackInfo ci) { - if (EventFlow.post(new WorldEvent.BlockUpdate(pos, state, flags)).isCanceled()) { - 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(); diff --git a/common/src/main/java/com/lambda/mixin/world/StructureTemplateMixin.java b/common/src/main/java/com/lambda/mixin/world/StructureTemplateMixin.java new file mode 100644 index 000000000..63960b453 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/world/StructureTemplateMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.world; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.block.Block; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryEntryLookup; +import net.minecraft.structure.StructureTemplate; +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.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Objects; + +@Mixin(StructureTemplate.class) +public class StructureTemplateMixin { + @Shadow + private String author; + + @ModifyReturnValue(method = "getAuthor()Ljava/lang/String;", at = @At("RETURN")) + public String getAuthor(String original) { + return Objects.equals(original, "?") || Objects.equals(original, "") ? "Unknown" : original; + } + + @Inject(method = "writeNbt(Lnet/minecraft/nbt/NbtCompound;)Lnet/minecraft/nbt/NbtCompound;", at = @At("TAIL")) + public void writeNbt(NbtCompound nbt, CallbackInfoReturnable cir) { + nbt.putString("author", author); + } + + @Inject(method = "readNbt(Lnet/minecraft/registry/RegistryEntryLookup;Lnet/minecraft/nbt/NbtCompound;)V", at = @At("TAIL")) + public void readNbt(RegistryEntryLookup blockLookup, NbtCompound nbt, CallbackInfo ci) { + author = nbt.getString("author"); + } +} diff --git a/common/src/main/java/com/lambda/mixin/world/WorldMixin.java b/common/src/main/java/com/lambda/mixin/world/WorldMixin.java new file mode 100644 index 000000000..bc809a5e8 --- /dev/null +++ b/common/src/main/java/com/lambda/mixin/world/WorldMixin.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.world; + +import com.lambda.event.EventFlow; +import com.lambda.event.events.WorldEvent; +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(World.class) +public abstract class WorldMixin { + @Inject(method = "onBlockChanged", at = @At("TAIL")) + void onBlockChanged(BlockPos pos, BlockState oldBlock, BlockState newBlock, CallbackInfo ci) { + EventFlow.post(new WorldEvent.BlockChange(pos, oldBlock, newBlock)); + } +} diff --git a/common/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt index 621b2f32d..cb2d81763 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/BuildCommand.kt @@ -17,18 +17,24 @@ package com.lambda.command.commands -import com.lambda.brigadier.argument.literal -import com.lambda.brigadier.execute +import com.lambda.brigadier.CommandResult +import com.lambda.brigadier.argument.* +import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.optional import com.lambda.brigadier.required import com.lambda.command.LambdaCommand -import com.lambda.interaction.construction.Blueprint.Companion.toStructure -import com.lambda.interaction.construction.DynamicBlueprint.Companion.toBlueprint -import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.construction.StructureRegistry +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint +import com.lambda.task.TaskFlow.run import com.lambda.task.tasks.BuildTask.Companion.build import com.lambda.threading.runSafe +import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder -import net.minecraft.block.Blocks -import net.minecraft.util.math.BlockBox +import com.lambda.util.extension.move +import java.nio.file.InvalidPathException +import java.nio.file.NoSuchFileException +import java.nio.file.Path object BuildCommand : LambdaCommand( name = "Build", @@ -37,26 +43,37 @@ object BuildCommand : LambdaCommand( ) { override fun CommandBuilder.create() { required(literal("place")) { - execute { - runSafe { - val materials = setOf( - TargetState.Block(Blocks.NETHERRACK), - TargetState.Block(Blocks.AIR), - TargetState.Block(Blocks.COBBLESTONE), - TargetState.Block(Blocks.AIR), - ) - val facing = player.horizontalFacing - val pos = player.blockPos.add(facing.vector.multiply(2)) + required(greedyString("structure")) { structure -> + suggests { _, builder -> + StructureRegistry.forEach { key, _ -> builder.suggest(key) } + builder.buildFuture() + } + executeWithResult { + val pathString = structure().value() + runSafe { + try { + StructureRegistry + .loadStructureByRelativePath(Path.of(pathString)) + ?.let { template -> + info("Building structure $pathString with dimensions ${template.size.toShortString()} created by ${template.author}") + template.toStructure() + .move(player.blockPos) + .toBlueprint() + .build() + .run() - BlockBox.create(pos, pos.add(facing.rotateYClockwise().vector.multiply(3))) - .toStructure(TargetState.Block(Blocks.NETHERRACK)) - .toBlueprint { - it.mapValues { (_, _) -> - materials.elementAt((System.currentTimeMillis() / 5000).toInt() % materials.size) - } + return@executeWithResult CommandResult.success() + } + } catch (e: InvalidPathException) { + return@executeWithResult CommandResult.failure("Invalid path $pathString") + } catch (e: NoSuchFileException) { + return@executeWithResult CommandResult.failure("Structure $pathString not found") + } catch (e: Exception) { + return@executeWithResult CommandResult.failure(e.message ?: "Failed to load structure $pathString") } - .build(finishOnDone = false) - .start(null) + } + + CommandResult.failure("Structure $pathString not found") } } } diff --git a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt index f5610f997..4d079f4cd 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -30,6 +30,7 @@ import com.lambda.module.modules.player.Replay import com.lambda.util.FolderRegister import com.lambda.util.FolderRegister.listRecursive import com.lambda.util.extension.CommandBuilder +import kotlin.io.path.exists object ReplayCommand : LambdaCommand( name = "replay", @@ -48,7 +49,7 @@ object ReplayCommand : LambdaCommand( required(literal("load")) { required(greedyString("replay filepath")) { replayName -> suggests { _, builder -> - val dir = FolderRegister.replay + val dir = FolderRegister.replay.toFile() dir.listRecursive { it.isFile }.forEach { builder.suggest(it.relativeTo(dir).path) } @@ -63,7 +64,7 @@ object ReplayCommand : LambdaCommand( } try { - Replay.loadRecording(replayFile) + Replay.loadRecording(replayFile.toFile()) } catch (e: JsonSyntaxException) { return@executeWithResult CommandResult.failure("Failed to load replay file: ${e.message}") } diff --git a/common/src/main/kotlin/com/lambda/command/commands/TaskCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/TaskCommand.kt index 41dd5fb7e..97c33383a 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/TaskCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/TaskCommand.kt @@ -21,20 +21,28 @@ import com.lambda.brigadier.argument.literal import com.lambda.brigadier.execute import com.lambda.brigadier.required import com.lambda.command.LambdaCommand -import com.lambda.task.RootTask +import com.lambda.task.TaskFlow import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder object TaskCommand : LambdaCommand( name = "task", - usage = "task ", + usage = "task ", description = "Control tasks" ) { override fun CommandBuilder.create() { required(literal("cancel")) { execute { this@TaskCommand.info("Cancelling all tasks") - RootTask.cancel() + TaskFlow.cancel() + } + } + + required(literal("clear")) { + execute { + this@TaskCommand.info("Clearing all tasks") + TaskFlow.cancel() + TaskFlow.clear() } } } diff --git a/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt index 4ab9ab51f..35ca83ec6 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/TransferCommand.kt @@ -23,10 +23,12 @@ 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.container.ContainerManager +import com.lambda.interaction.material.container.ContainerManager.containerWithMaterial +import com.lambda.interaction.material.container.ContainerManager.containerWithSpace import com.lambda.interaction.material.StackSelection.Companion.selectStack import com.lambda.interaction.material.transfer.TransferResult +import com.lambda.task.TaskFlow.run import com.lambda.util.Communication.info import com.lambda.util.extension.CommandBuilder @@ -35,21 +37,19 @@ object TransferCommand : LambdaCommand( usage = "transfer ", description = "Transfer items from anywhere to anywhere", ) { - private var lastTransfer: TransferResult.Transfer? = null + private var lastContainerTransfer: TransferResult.ContainerTransfer? = null override fun CommandBuilder.create() { required(itemStack("stack", registry)) { stack -> - required(integer("amount")) { amount -> + required(integer("amount", 1)) { 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\"") + containerWithMaterial(selection).forEachIndexed { i, container -> + builder.suggest("\"${i + 1}. ${container.name}\"", container.description(selection)) } builder.buildFuture() } @@ -58,10 +58,8 @@ object TransferCommand : LambdaCommand( 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\"") + containerWithSpace(selection).forEachIndexed { i, container -> + builder.suggest("\"${i + 1}. ${container.name}\"", container.description(selection)) } builder.buildFuture() } @@ -70,25 +68,25 @@ object TransferCommand : LambdaCommand( isItem(stack().value().item) } val fromContainer = ContainerManager.container().find { - it.name == from().value().split(" with ").firstOrNull() + it.name == from().value().split(".").last().trim() } ?: return@executeWithResult failure("From container not found") val toContainer = ContainerManager.container().find { - it.name == to().value().split(" with ").firstOrNull() + it.name == to().value().split(".").last().trim() } ?: return@executeWithResult failure("To container not found") - when (val result = fromContainer.transfer(selection, toContainer)) { - is TransferResult.Transfer -> { - info("$result started.") - lastTransfer = result - result.onSuccess { _, _ -> - info("$lastTransfer completed.") - }.start(null) + when (val transaction = fromContainer.transfer(selection, toContainer)) { + is TransferResult.ContainerTransfer -> { + info("${transaction.name} started.") + lastContainerTransfer = transaction + transaction.finally { + info("${transaction.name} completed.") + }.run() return@executeWithResult success() } is TransferResult.MissingItems -> { - return@executeWithResult failure("Missing items: ${result.missing}") + return@executeWithResult failure("Missing items: ${transaction.missing}") } is TransferResult.NoSpace -> { @@ -105,11 +103,11 @@ object TransferCommand : LambdaCommand( required(literal("cancel")) { executeWithResult { - lastTransfer?.cancel() ?: run { + lastContainerTransfer?.cancel() ?: run { return@executeWithResult failure("No transfer to cancel") } - info("$lastTransfer cancelled") - lastTransfer = null + info("$lastContainerTransfer cancelled") + lastContainerTransfer = null success() } } 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 e224f828c..92d0b8bea 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/FriendConfig.kt @@ -19,8 +19,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration import com.lambda.util.FolderRegister +import java.io.File object FriendConfig : Configuration() { override val configName get() = "friends" - override val primary = FolderRegister.config.resolve("$configName.json") + override val primary: File = FolderRegister.config.resolve("$configName.json").toFile() } 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 592c37bdf..1793bf5c9 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/GuiConfig.kt @@ -19,8 +19,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration import com.lambda.util.FolderRegister +import java.io.File object GuiConfig : Configuration() { override val configName get() = "gui" - override val primary = FolderRegister.config.resolve("$configName.json") + override val primary: File = FolderRegister.config.resolve("$configName.json").toFile() } 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 be717ec42..4f03e3449 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/LambdaConfig.kt @@ -19,8 +19,9 @@ package com.lambda.config.configurations import com.lambda.config.Configuration import com.lambda.util.FolderRegister +import java.io.File object LambdaConfig : Configuration() { override val configName get() = "lambda" - override val primary = FolderRegister.config.resolve("$configName.json") + override val primary: File = FolderRegister.config.resolve("$configName.json").toFile() } 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 e8410194d..01e235f37 100644 --- a/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/configurations/ModuleConfig.kt @@ -19,6 +19,7 @@ package com.lambda.config.configurations import com.lambda.config.Configuration import com.lambda.util.FolderRegister +import java.io.File /** @@ -31,5 +32,5 @@ import com.lambda.util.FolderRegister */ object ModuleConfig : Configuration() { override val configName get() = "modules" - override val primary = FolderRegister.config.resolve("$configName.json") + override val primary: File = FolderRegister.config.resolve("$configName.json").toFile() } diff --git a/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt index db143825a..5b3aa74b0 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt @@ -17,13 +17,27 @@ package com.lambda.config.groups +import net.minecraft.block.Block + interface BuildConfig { - val breakConfirmation: Boolean - val placeConfirmation: Boolean - val collectDrops: Boolean - val breakWeakBlocks: Boolean + // General val pathing: Boolean - val breaksPerTick: Int + val stayInRange: Boolean + val collectDrops: Boolean + + // Breaking val rotateForBreak: Boolean + val breakConfirmation: Boolean + val maxPendingBreaks: Int + val breaksPerTick: Int + val breakWeakBlocks: Boolean + val forceSilkTouch: Boolean + val ignoredBlocks: Set + + // Placing val rotateForPlace: Boolean + val placeConfirmation: Boolean + val placeTimeout: Int + val maxPendingPlacements: Int + val placementsPerTick: Int } diff --git a/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt index f9e70ae7b..89a6deabf 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -18,6 +18,8 @@ package com.lambda.config.groups import com.lambda.config.Configurable +import com.lambda.module.modules.client.TaskFlowModule.setting +import com.lambda.util.BlockUtils.allSigns class BuildSettings( c: Configurable, @@ -29,16 +31,24 @@ class BuildSettings( private val page by c.setting("Build Page", Page.GENERAL, "Current page", vis) + // General override val pathing by c.setting("Pathing", true, "Path to blocks") { vis() && page == Page.GENERAL } + override val stayInRange by c.setting("Stay In Range", true, "Stay in range of blocks") { vis() && page == Page.GENERAL && pathing } + override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks") { vis() && page == Page.GENERAL } + // Breaking + override val rotateForBreak by c.setting("Rotate For Break", true, "Rotate towards block while breaking") { vis() && page == Page.BREAK } override val breakConfirmation by c.setting("Break Confirmation", false, "Wait for block break confirmation") { vis() && page == Page.BREAK } + override val maxPendingBreaks by c.setting("Max Pending Breaks", 1, 1..10, 1, "Maximum pending block breaks") { vis() && page == Page.BREAK } + override val breaksPerTick by c.setting("Instant Breaks Per Tick", 5, 1..30, 1, "Maximum instant block breaks per tick") { 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", true, "Rotate towards block while breaking") { vis() && page == Page.BREAK } - - override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks") { vis() && page == Page.BREAK } - - override val placeConfirmation by c.setting("Place Confirmation", true, "Wait for block placement confirmation") { vis() && page == Page.PLACE } + override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { vis() && page == Page.BREAK } + override val ignoredBlocks by setting("Ignored Blocks", allSigns, "Blocks that wont be broken") { vis() && page == Page.BREAK } + // Placing override val rotateForPlace by c.setting("Rotate For Place", true, "Rotate towards block while placing") { vis() && page == Page.PLACE } + override val placeConfirmation by c.setting("Place Confirmation", true, "Wait for block placement confirmation") { vis() && page == Page.PLACE } + override val placeTimeout by c.setting("Place Timeout", 10, 1..30, 1, "Timeout for block placement in ticks", unit = " ticks") { vis() && page == Page.PLACE && placeConfirmation } + override val maxPendingPlacements by c.setting("Max Pending Places", 1, 1..10, 1, "Maximum pending block places") { vis() && page == Page.PLACE } + override val placementsPerTick by c.setting("Instant Places Per Tick", 1, 1..30, 1, "Maximum instant block places per tick") { vis() && page == Page.PLACE } } diff --git a/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt index 63d45b4be..4b1a946fb 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/InteractionConfig.kt @@ -25,6 +25,8 @@ interface InteractionConfig { */ val reach: Double + val visibilityCheck: Boolean + /** * Will check `resolution squared` many points on a grid on each visible surface of the hit box. */ diff --git a/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt index 5d0691e83..e528607af 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/InteractionSettings.kt @@ -26,6 +26,7 @@ class InteractionSettings( ) : InteractionConfig { override val reach by c.setting("Reach", defaultReach, 0.1..10.0, 0.1, "Players reach / range", " blocks", vis) override val useRayCast by c.setting("Raycast", true, "Verify hit vector with ray casting (for very strict ACs)", vis) + override val visibilityCheck by c.setting("Visibility Check", true, "Check if target is visible", vis) override val resolution by c.setting("Resolution", 4, 1..40, 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) override val pingTimeout by c.setting("Ping Timeout", false, "Timeout on high ping", vis) diff --git a/common/src/main/kotlin/com/lambda/config/groups/InventoryConfig.kt b/common/src/main/kotlin/com/lambda/config/groups/InventoryConfig.kt new file mode 100644 index 000000000..4f068b8df --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/InventoryConfig.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.interaction.material.StackSelection +import net.minecraft.block.Block + +interface InventoryConfig { + val disposables: Set + val accessEnderChest: Boolean + + val actionTimout: Int + val swapWithDisposables: Boolean + + val providerPriority: Priority + val storePriority: Priority + + enum class Priority { + WITH_MIN_ITEMS, + WITH_MAX_ITEMS; + + fun materialComparator(selection: StackSelection) = + when (this) { + WITH_MAX_ITEMS -> compareBy { it.rank } + .thenByDescending { it.materialAvailable(selection) } + .thenBy { it.name } + WITH_MIN_ITEMS -> compareBy { it.rank } + .thenBy { it.materialAvailable(selection) } + .thenBy { it.name } + } + + fun spaceComparator(selection: StackSelection) = + when (this) { + WITH_MAX_ITEMS -> compareBy { it.rank } + .thenByDescending { it.spaceAvailable(selection) } + .thenBy { it.name } + WITH_MIN_ITEMS -> compareBy { it.rank } + .thenBy { it.spaceAvailable(selection) } + .thenBy { it.name } + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt b/common/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt new file mode 100644 index 000000000..74f962802 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.groups + +import com.lambda.config.Configurable +import com.lambda.util.item.ItemUtils + +class InventorySettings( + c: Configurable, + vis: () -> Boolean = { true }, +) : InventoryConfig { + override val disposables by c.setting("Disposables", ItemUtils.defaultDisposables, "Items that will be ignored when checking for a free slot", vis) + override val accessEnderChest by c.setting("Access Ender Chest", false, "Allow access to the player's ender chest", vis) + override val actionTimout by c.setting("Action Timeout", 10, 0..100, 1, "How long to wait for after each inventory action", " ticks", vis) + override val swapWithDisposables by c.setting("Swap With Disposables", true, "Swap items with disposable ones", vis) + override val providerPriority by c.setting("Provider Priority", InventoryConfig.Priority.WITH_MIN_ITEMS, "What container to prefer when retrieving the item from", vis) + override val storePriority by c.setting("Store Priority", InventoryConfig.Priority.WITH_MIN_ITEMS, "What container to prefer when storing the item to", vis) +} \ 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/RotationConfig.kt similarity index 95% rename from common/src/main/kotlin/com/lambda/config/groups/IRotationConfig.kt rename to common/src/main/kotlin/com/lambda/config/groups/RotationConfig.kt index fc83f9eed..a0e4d4b81 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/IRotationConfig.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/RotationConfig.kt @@ -19,7 +19,7 @@ package com.lambda.config.groups import com.lambda.interaction.rotation.RotationMode -interface IRotationConfig { +interface RotationConfig { /** * - [RotationMode.SILENT] Spoofing server-side rotation. * - [RotationMode.SYNC] Spoofing server-side rotation and adjusting client-side movement based on reported rotation (for Grim). @@ -42,7 +42,7 @@ interface IRotationConfig { */ val resetTicks: Int - interface Instant : IRotationConfig { + interface Instant : RotationConfig { override val turnSpeed get() = 360.0 override val keepTicks get() = 1 override val resetTicks get() = 1 diff --git a/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt index ae112bfe3..8981c468c 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/RotationSettings.kt @@ -25,7 +25,7 @@ import kotlin.random.Random class RotationSettings( c: Configurable, vis: () -> Boolean = { true }, -) : IRotationConfig { +) : RotationConfig { /** * The rotation mode */ diff --git a/common/src/main/kotlin/com/lambda/event/EventFlow.kt b/common/src/main/kotlin/com/lambda/event/EventFlow.kt index 0b66b1478..9abf905a8 100644 --- a/common/src/main/kotlin/com/lambda/event/EventFlow.kt +++ b/common/src/main/kotlin/com/lambda/event/EventFlow.kt @@ -87,6 +87,11 @@ object EventFlow { */ val concurrentListeners = Subscriber() + fun Any.unsubscribe() { + syncListeners.unsubscribe(this) + concurrentListeners.unsubscribe(this) + } + init { // parallel event execution on dedicated threads runConcurrent { diff --git a/common/src/main/kotlin/com/lambda/event/Subscriber.kt b/common/src/main/kotlin/com/lambda/event/Subscriber.kt index 3a78dcf4a..6b9354a7c 100644 --- a/common/src/main/kotlin/com/lambda/event/Subscriber.kt +++ b/common/src/main/kotlin/com/lambda/event/Subscriber.kt @@ -54,6 +54,18 @@ class Subscriber : ConcurrentHashMap, ConcurrentSkipListSet
  • unsubscribe(listener: Listener) = getOrElse(T::class) { defaultListenerSet }.remove(listener) + /** + * Unsubscribes all listeners associated with the current instance (the caller object). + * This method iterates over all values in the `Subscriber`'s map and removes listeners + * whose `owner` property matches the caller object. + * + * Use this method when you want to clean up listeners that were registered with the + * current instance, preventing further event notifications. + */ + fun unsubscribe(owner: Any) { + values.forEach { it.removeAll { listener -> listener.owner == owner } } + } + /** Allows a [Subscriber] to stop receiving all [Event]s of another [Subscriber] */ infix fun unsubscribe(subscriber: Subscriber) { subscriber.forEach { (eventType, listeners) -> diff --git a/common/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt b/common/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt new file mode 100644 index 000000000..e9d3dc607 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.event.events + +import com.lambda.event.Event +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenHandler + +/** + * Represents various events related to inventory interactions, updates, and state changes. + * + * This sealed class encompasses different types of inventory events that occur during gameplay, + * such as opening or closing an inventory, updating inventory contents, or changing hotbar slots. + * Each event provides specific contextual data relevant to the inventory interaction. + */ +sealed class InventoryEvent { + /** + * Represents an event triggered when an inventory or screen is opened. + * + * This event provides access to the associated [ScreenHandler], which manages the interaction + * logic between the inventory and the player. It is typically used to initialize or handle + * logic when the screen linked to a player's inventory is opened. + * + * @property screenHandler The screen handler associated with the opened inventory. + */ + class Open(val screenHandler: ScreenHandler) : Event + + /** + * Represents an event triggered when an inventory or screen is closed. + * + * This event provides access to the associated [ScreenHandler], which manages the interaction + * logic between the inventory and the player. It is typically used to finalize or clean up logic + * when a player's inventory screen is closed. + * + * @property screenHandler The screen handler associated with the closed inventory. + */ + class Close(val screenHandler: ScreenHandler) : Event + + /** + * Represents an update event for an inventory, typically triggered when inventory contents or states + * change during gameplay. + * + * @property revision The revision number indicating the current state of the inventory. + * @property stacks A list of item stacks representing the contents of the inventory slots. + * @property cursorStack The item stack currently held by the cursor. + */ + data class FullUpdate( + val revision: Int, + val stacks: List, + val cursorStack: ItemStack, + ) : Event + + /** + * Represents an event triggered when a specific inventory slot is updated. + * + * @property syncId The synchronization ID related to the container or inventory. + * @property revision The revision number representing the state of the inventory. + * @property slot The index of the updated inventory slot. + * @property stack The new item stack in the updated slot. + */ + class SlotUpdate( + val syncId: Int, + val revision: Int, + val slot: Int, + val stack: ItemStack + ) : Event + + /** + * Represents an event triggered when a player switches their selected hotbar slot. + * + * @property slot The index of the newly selected hotbar slot. + */ + class SelectedHotbarSlotUpdate(val slot: Int) : Event +} \ 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 f7be6c13c..3679a973c 100644 --- a/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/WorldEvent.kt @@ -21,7 +21,6 @@ import com.lambda.event.Event 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.entity.data.TrackedData import net.minecraft.util.math.BlockPos @@ -46,14 +45,11 @@ sealed class WorldEvent { ) : Event } - /** - * Represents a block update in the world - */ - class BlockUpdate( + class BlockChange( val pos: BlockPos, - val state: BlockState, - val flags: Int - ) : ICancellable by Cancellable() + val oldState: BlockState, + val newState: BlockState, + ) : Event /** * Represents an entity being added to the world diff --git a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt index 49611d4cb..1c7cd869b 100644 --- a/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt +++ b/common/src/main/kotlin/com/lambda/event/listener/SafeListener.kt @@ -46,9 +46,9 @@ import kotlin.reflect.KProperty * The [SafeListener] will keep a reference to the last signal processed by the listener. * Allowing use cases where the last signal is needed. * ```kotlin - * val lastPacketReceived by listener() + * val lastPacketReceived by listen() * - * listener { event -> + * listen { event -> * println("Last packet received: ${lastPacketReceived?.packet}") * // prints the last packet received * // prints null if no packet was received @@ -130,51 +130,6 @@ class SafeListener( return listener } - /** - * Registers a new [SafeListener] for a generic [Event] type [T] within the context of a [Task]. - * The [function] is executed on the same thread where the [Event] was dispatched. - * The [function] will only be executed when the context satisfies certain safety conditions. - * These conditions are met when none of the following [SafeContext] properties are null: - * - [SafeContext.world] - * - [SafeContext.player] - * - [SafeContext.interaction] - * - [SafeContext.connection] - * - * Usage: - * ```kotlin - * myTask.listen { event -> - * player.sendMessage("Event received: $event") - * } - * - * myTask.listen(priority = 1) { event -> - * player.sendMessage("Event received before the previous listener: $event") - * } - * ``` - * - * @param T The type of the event to listen for. - * This should be a subclass of Event. - * @param priority The priority of the listener. - * Listeners with higher priority will be executed first. - * The Default value is 0. - * @param alwaysListen If true, the listener will be executed even if it is muted. The Default value is false. - * @param function The function to be executed when the event is posted. - * This function should take a SafeContext and an event of type T as parameters. - * @return The newly created and registered [SafeListener]. - */ - inline fun Task<*>.listen( - priority: Int = 0, - alwaysListen: Boolean = false, - noinline function: SafeContext.(T) -> Unit = {}, - ): SafeListener { - val listener = SafeListener(priority, this, alwaysListen) { event -> - function(event) // ToDo: run function always on game thread - } - - syncListeners.subscribe(listener) - - return listener - } - /** * This function registers a new [SafeListener] for a generic [Event] type [T]. * The [transform] is executed on the same thread where the [Event] was dispatched. diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt index 6210c599d..8e5919078 100644 --- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt +++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt @@ -53,7 +53,7 @@ class ChunkedESP private constructor( } init { - listenConcurrently { event -> + listenConcurrently { event -> world.getWorldChunk(event.pos).renderer.notifyChunks() } diff --git a/common/src/main/kotlin/com/lambda/http/Request.kt b/common/src/main/kotlin/com/lambda/http/Request.kt index 9a29f1781..c2a8994b8 100644 --- a/common/src/main/kotlin/com/lambda/http/Request.kt +++ b/common/src/main/kotlin/com/lambda/http/Request.kt @@ -55,7 +55,7 @@ data class Request( * @param maxAge The maximum age of the cached resource. Default is 4 days. */ fun maybeDownload(name: String, maxAge: Duration = 7.days): File { - val file = cache.resolve(name).createIfNotExists() + val file = cache.resolve(name).toFile().createIfNotExists() if ( System.currentTimeMillis() - file.lastModified() < maxAge.inWholeMilliseconds diff --git a/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt b/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt index 188967335..3e27cedf0 100644 --- a/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/RotationManager.kt @@ -22,7 +22,9 @@ import com.lambda.config.groups.RotationSettings import com.lambda.context.SafeContext import com.lambda.core.Loadable import com.lambda.event.EventFlow.post -import com.lambda.event.events.* +import com.lambda.event.events.ConnectionEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.events.RotationEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe import com.lambda.interaction.rotation.Rotation @@ -34,14 +36,17 @@ import com.lambda.interaction.rotation.RotationMode import com.lambda.module.modules.client.Baritone import com.lambda.threading.runGameScheduled import com.lambda.threading.runSafe -import com.lambda.util.math.lerp -import com.lambda.util.math.MathUtils.toRadian -import com.lambda.util.math.Vec2d import com.lambda.util.extension.partialTicks import com.lambda.util.extension.rotation +import com.lambda.util.math.MathUtils.toRadian +import com.lambda.util.math.Vec2d +import com.lambda.util.math.lerp import net.minecraft.client.input.Input import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket -import kotlin.math.* +import kotlin.math.cos +import kotlin.math.round +import kotlin.math.sign +import kotlin.math.sin object RotationManager : Loadable { var currentRotation = Rotation.ZERO @@ -54,16 +59,17 @@ object RotationManager : Loadable { override fun load() = "Loaded Rotation Manager" - fun Any.requestRotation( + @RotationDsl + fun Any.rotate( priority: Int = 0, alwaysListen: Boolean = false, - onUpdate: SafeContext.(lastContext: RotationContext?) -> RotationContext?, - onReceive: SafeContext.() -> Unit = {} + block: RequestRotationBuilder.() -> Unit, ) { + val builder = RequestRotationBuilder().apply(block) var lastCtx: RotationContext? = null - this.listen(priority, alwaysListen) { event -> - val rotationContext = onUpdate(event.context) + listen(priority, alwaysListen) { event -> + val rotationContext = builder.onUpdate?.invoke(this, event.context) rotationContext?.let { event.context = it @@ -72,13 +78,31 @@ object RotationManager : Loadable { lastCtx = rotationContext } - this.listen { event -> - if (event.context == lastCtx && event.context.isValid) { - onReceive() + listen { event -> + if (event.context == lastCtx) { + builder.onReceive?.invoke(this, event.context) } } } + @DslMarker + annotation class RotationDsl + + class RequestRotationBuilder { + var onUpdate: (SafeContext.(lastContext: RotationContext?) -> RotationContext?)? = null + var onReceive: (SafeContext.(context: RotationContext) -> Unit)? = null + + @RotationDsl + fun onUpdate(block: SafeContext.(lastContext: RotationContext?) -> RotationContext?) { + onUpdate = block + } + + @RotationDsl + fun onReceive(block: SafeContext.(context: RotationContext) -> Unit) { + onReceive = block + } + } + @JvmStatic fun update() = runSafe { RotationEvent.Update(BaritoneProcessor.poolContext()).post { diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/StructureRegistry.kt b/common/src/main/kotlin/com/lambda/interaction/construction/StructureRegistry.kt new file mode 100644 index 000000000..4d4003f5b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/StructureRegistry.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction + +import com.lambda.Lambda.LOG +import com.lambda.core.Loadable +import com.lambda.util.Communication.logError +import com.lambda.util.FolderRegister +import com.lambda.util.extension.readLitematicaOrException +import com.lambda.util.extension.readNbtOrException +import com.lambda.util.extension.readSchematicOrException +import com.lambda.util.extension.readSpongeOrException +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtIo +import net.minecraft.nbt.NbtSizeTracker +import net.minecraft.registry.Registries +import net.minecraft.structure.StructureTemplate +import java.nio.file.* +import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE +import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.* + +/** + * The `StructureRegistry` object is responsible for managing the loading, saving, and validating of Minecraft structure templates. + * It extends [ConcurrentHashMap] to allow concurrent access to structure templates by their names. + * This registry supports multiple structure formats and automatically monitors changes in the structure directory. + */ +@OptIn(ExperimentalPathApi::class) +@Suppress("JavaIoSerializableObjectMustHaveReadResolve") +object StructureRegistry : ConcurrentHashMap(), Loadable { + private val pathWatcher by lazy { + FileSystems.getDefault().newWatchService() + .apply { FolderRegister.structure.register(this, ENTRY_CREATE, ENTRY_DELETE) } + } + + /** + * Map of file suffix to their respective read function + */ + private val serializers = mapOf( + "nbt" to StructureTemplate::readNbtOrException, + "schem" to StructureTemplate::readSpongeOrException, + "litematica" to StructureTemplate::readLitematicaOrException, + + // Not supported, who could guess that converting a format from 14 years ago would be hard? :clueless: + "schematic" to StructureTemplate::readSchematicOrException, + ) + + /** + * Loads a structure by relative path, will attempt a discovery sequence if the structure could not be found + * and performs format conversions if necessary + * + * @param relativePath The name of the structure to load (without extension). + * @param convert Whether to replace the file after converting it. + * @return The loaded [StructureTemplate], or null if the structure is not found. + */ + fun loadStructureByRelativePath( + relativePath: Path, + convert: Boolean = true, + ): StructureTemplate? { + updateFileWatcher() + + return computeIfAbsent(relativePath.pathString.lowercase()) { + loadFileAndCreate(relativePath, convert) + } + } + + /** + * Poll directory file events to load many structures at once. + * They might not show up in the command suggestion, but they are + * present in the map. + */ + private fun updateFileWatcher() { + pathWatcher.poll()?.let { key -> + key.pollEvents() + ?.filterIsInstance>() + ?.forEach { event -> + val newPath = event.context() + + when (event.kind()) { + ENTRY_DELETE -> remove(newPath.pathString) + ENTRY_CREATE -> { + computeIfAbsent(newPath.pathString) { + loadStructureByRelativePath(newPath, convert = true) + } + } + } + + // Reset the key -- this step is critical if you want to + // receive further watch events. If the key is no longer valid, + // the directory is inaccessible so exit the loop. + if (!key.reset()) return@forEach + } + } + } + + /** + * Loads the structure file and creates a [StructureTemplate]. + * + * @param convert Whether to replace the file after converting it. + * @return The created [StructureTemplate], or null if the structure is not found or invalid. + */ + private fun loadFileAndCreate(path: Path, convert: Boolean) = + FolderRegister.structure.resolve(path).inputStream().use { templateStream -> + val compound = NbtIo.readCompressed(templateStream, NbtSizeTracker.ofUnlimitedBytes()) + val extension = path.extension + val template = createStructure(compound, extension) + + if (convert && extension != "nbt") { + template?.let { saveStructure(path.pathString, it) } + } + + // Verify the structure integrity after it had been + // converted to a regular structure template + if (compound.isValidStructureTemplate()) { + template + } else { + logError("Corrupted structure file: ${path.pathString}") + null + } + } + + /** + * Creates a [StructureTemplate] from the provided NBT data. + * + * @param nbt The [NbtCompound] containing the structure's data. + * @return The created [StructureTemplate], or null if there was an error. + */ + private fun createStructure(nbt: NbtCompound, suffix: String): StructureTemplate? = + StructureTemplate().apply { + serializers[suffix] + ?.invoke(this, Registries.BLOCK.readOnlyWrapper, nbt) + ?.let { error -> + logError("Could not create structure from file: ${error.message}") + return null + } + } + + /** + * Saves the provided [structure] to disk under the specified [name]. + * + * @param relativePath The relative path of the structure to save. + * @param structure The [StructureTemplate] to save. + */ + private fun saveStructure(relativePath: String, structure: StructureTemplate) { + val path = FolderRegister.structure.resolve(relativePath) + val compound = structure.writeNbt(NbtCompound()) + + Files.createDirectories(path.parent) + path.outputStream().use { output -> + NbtIo.writeCompressed(compound, output) + } + } + + /** + * Verifies that the provided NBT data represents a valid Minecraft structure template. + * + * @receiver The [NbtCompound] to validate. + * @return True if the NBT contains valid structure template data, false otherwise. + */ + private fun NbtCompound.isValidStructureTemplate() = + contains("DataVersion") && contains("blocks") && contains("palette") && contains("size") + + override fun load(): String { + FolderRegister.structure.walk() + .filter { it.extension in serializers.keys } + .forEach { loadStructureByRelativePath(FolderRegister.structure.relativize(it)) } + + return "Loaded $size structure templates" + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/Blueprint.kt similarity index 65% rename from common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/blueprint/Blueprint.kt index 3706a2210..136ba7aea 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/Blueprint.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/Blueprint.kt @@ -15,30 +15,38 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction +package com.lambda.interaction.construction.blueprint -import com.lambda.context.SafeContext import com.lambda.interaction.construction.verify.TargetState -import com.lambda.module.modules.client.TaskFlow import com.lambda.util.BlockUtils.blockPos -import com.lambda.util.BlockUtils.blockState -import com.lambda.util.Communication.info import com.lambda.util.extension.Structure +import com.lambda.util.math.VecUtils.blockPos import net.minecraft.structure.StructureTemplate -import net.minecraft.util.math.BlockBox -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box +import net.minecraft.util.math.* abstract class Blueprint { abstract val structure: Structure - open fun isDone(ctx: SafeContext) = - structure.all { (pos, targetState) -> - with(ctx) { - val state = pos.blockState(world) - targetState.matches(state, pos, world) || state.block in TaskFlow.ignoredBlocks - } - } + private val bounds: BlockBox by lazy { + val maxX = structure.keys.maxOf { it.x } + val maxY = structure.keys.maxOf { it.y } + val maxZ = structure.keys.maxOf { it.z } + val minX = structure.keys.minOf { it.x } + val minY = structure.keys.minOf { it.y } + val minZ = structure.keys.minOf { it.z } + BlockBox(minX, minY, minZ, maxX, maxY, maxZ) + } + + fun getClosestPointTo(target: Vec3d): Vec3d { + val d = MathHelper.clamp(target.x, bounds.minX.toDouble(), bounds.maxX.toDouble()) + val e = MathHelper.clamp(target.y, bounds.minY.toDouble(), bounds.maxY.toDouble()) + val f = MathHelper.clamp(target.z, bounds.minZ.toDouble(), bounds.maxZ.toDouble()) + return Vec3d(d, e, f) + } + + fun isOutOfBounds(vec3d: Vec3d): Boolean = !bounds.contains(vec3d.blockPos) + + val center get() = bounds.center.blockPos companion object { fun emptyStructure(): Structure = emptyMap() diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/DynamicBlueprint.kt similarity index 81% rename from common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/blueprint/DynamicBlueprint.kt index 27a45e036..27908f5ff 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/DynamicBlueprint.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/DynamicBlueprint.kt @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction +package com.lambda.interaction.construction.blueprint import com.lambda.context.SafeContext +import com.lambda.threading.runSafe import com.lambda.util.extension.Structure import net.minecraft.util.math.Vec3i @@ -25,17 +26,23 @@ 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 update() { + runSafe { + structure = update(structure) + } } - fun create(ctx: SafeContext) { - structure = ctx.init(structure) + fun create() { + runSafe { + structure = init(structure) + } } override var structure: Structure = emptyMap() private set + override fun toString() = "Dynamic Blueprint at ${center.toShortString()}" + companion object { fun offset(offset: Vec3i): SafeContext.(Structure) -> Structure = { it.map { (pos, state) -> diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt similarity index 87% rename from common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt index f276efc47..b41f806d0 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/StaticBlueprint.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/blueprint/StaticBlueprint.kt @@ -15,13 +15,15 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction +package com.lambda.interaction.construction.blueprint import com.lambda.util.extension.Structure data class StaticBlueprint( override val structure: Structure ) : Blueprint() { + override fun toString() = "Static Blueprint at ${center.toShortString()}" + companion object { fun Structure.toBlueprint() = StaticBlueprint(this) } 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 index 8cc5b1887..8f99ffe26 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt @@ -35,7 +35,7 @@ data class BreakContext( override var hand: Hand, val instantBreak: Boolean, ) : BuildContext { - override val resultingPos: BlockPos + override val expectedPos: BlockPos get() = result.blockPos override val distance: Double by lazy { @@ -58,4 +58,8 @@ data class BreakContext( else -> 1 } } + + override fun SafeContext.buildRenderer() { + + } } 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 index 8483777a2..316ea1b84 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt @@ -17,6 +17,7 @@ package com.lambda.interaction.construction.context +import com.lambda.interaction.construction.result.Drawable import com.lambda.interaction.rotation.RotationContext import net.minecraft.block.BlockState import net.minecraft.util.Hand @@ -24,14 +25,14 @@ import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d -interface BuildContext : Comparable { +interface BuildContext : Comparable, Drawable { val pov: Vec3d val result: BlockHitResult val distance: Double val expectedState: BlockState + val expectedPos: BlockPos val checkedState: BlockState val hand: Hand - val resultingPos: BlockPos val rotation: RotationContext override fun compareTo(other: BuildContext): Int { 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 index e6804360a..83481b88d 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt @@ -17,14 +17,21 @@ package com.lambda.interaction.construction.context +import com.lambda.context.SafeContext +import com.lambda.graphics.renderer.esp.DirectionMask +import com.lambda.graphics.renderer.esp.DirectionMask.exclude import com.lambda.interaction.construction.verify.TargetState import com.lambda.interaction.rotation.RotationContext +import com.lambda.threading.runSafe import com.lambda.util.BlockUtils +import com.lambda.util.Communication.warn 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 +import java.awt.Color data class PlaceContext( override val pov: Vec3d, @@ -34,15 +41,39 @@ data class PlaceContext( override val expectedState: BlockState, override val checkedState: BlockState, override val hand: Hand, + override val expectedPos: BlockPos, val targetState: TargetState, val sneak: Boolean, val insideBlock: Boolean, + val primeDirection: Direction?, ) : BuildContext { - override val resultingPos: BlockPos - get() = result.blockPos.offset(result.side) + var placeTick = 0L + private val baseColor = Color(35, 188, 254, 25) + private val sideColor = Color(35, 188, 254, 100) - override fun compareTo(other: BuildContext): Int { - return when (other) { + fun place(swingHand: Boolean) { + runSafe { + val actionResult = interaction.interactBlock( + player, hand, result + ) + + if (actionResult.isAccepted) { + if (actionResult.shouldSwingHand() && swingHand) { + player.swingHand(hand) + } + + if (!player.getStackInHand(hand).isEmpty && interaction.hasCreativeInventory()) { + mc.gameRenderer.firstPersonRenderer.resetEquipProgress(hand) + } + placeTick = mc.uptimeInTicks + } else { + warn("Internal interaction failed with $actionResult") + } + } + } + + override fun compareTo(other: BuildContext) = + when (other) { is PlaceContext -> compareBy { BlockUtils.fluids.indexOf(it.checkedState.fluidState.fluid) }.thenByDescending { @@ -59,5 +90,9 @@ data class PlaceContext( else -> 1 } + + override fun SafeContext.buildRenderer() { + withState(expectedState, expectedPos, baseColor, DirectionMask.ALL.exclude(result.side.opposite)) + withState(expectedState, expectedPos, sideColor, result.side.opposite) } } diff --git a/common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/PlacementProcessor.kt similarity index 61% rename from common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/processing/PlacementProcessor.kt index df017b316..31fde9586 100644 --- a/common/src/main/kotlin/com/lambda/event/events/ScreenHandlerEvent.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/PlacementProcessor.kt @@ -15,19 +15,12 @@ * along with this program. If not, see . */ -package com.lambda.event.events +package com.lambda.interaction.construction.processing -import com.lambda.event.Event -import net.minecraft.item.ItemStack -import net.minecraft.screen.ScreenHandler +import net.minecraft.block.BlockState -sealed class ScreenHandlerEvent { - class Open(val screenHandler: ScreenHandler) : Event - class Close(val screenHandler: ScreenHandler) : Event - - data class Update( - val revision: Int, - val stacks: List, - val cursorStack: ItemStack, - ) : Event +abstract class PlacementProcessor { + abstract fun acceptState(state: BlockState): Boolean + abstract fun preProcess(state: BlockState): PreprocessingStep } + diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/PreprocessingStep.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/PreprocessingStep.kt new file mode 100644 index 000000000..bd1e566c0 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/PreprocessingStep.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing + +import com.lambda.interaction.construction.verify.SurfaceScan +import net.minecraft.state.property.Property +import net.minecraft.util.math.Direction + +data class PreprocessingStep( + val surfaceScan: SurfaceScan = SurfaceScan.DEFAULT, + val ignore: Set> = emptySet(), + val sides: Set = Direction.entries.toSet(), +) \ 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/construction/processing/ProcessingStep.kt similarity index 93% rename from common/src/main/kotlin/com/lambda/interaction/processing/ProcessingStep.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessingStep.kt index 7fc258826..23fa2722c 100644 --- a/common/src/main/kotlin/com/lambda/interaction/processing/ProcessingStep.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessingStep.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.interaction.processing +package com.lambda.interaction.construction.processing import com.lambda.task.Task diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt new file mode 100644 index 000000000..dc70e7844 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing + +import com.lambda.core.Loadable +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.util.reflections.getInstances +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks + +object ProcessorRegistry : Loadable { + private const val PROCESSOR_PACKAGE = "com.lambda.interaction.construction.processing.processors" + private val processors = getInstances { forPackages(PROCESSOR_PACKAGE) } + private val processorCache = mutableMapOf() + + override fun load() = "Loaded ${processors.size} pre processors" + + fun TargetState.findProcessorForState(): PreprocessingStep = + (this as? TargetState.State)?.let { state -> + processorCache.getOrPut(state.blockState) { + (processors.find { it.acceptState(state.blockState) } ?: DefaultProcessor).preProcess(state.blockState) + } + } ?: DefaultProcessor.preProcess(Blocks.AIR.defaultState) + + object DefaultProcessor : PlacementProcessor() { + override fun acceptState(state: BlockState) = true + override fun preProcess(state: BlockState) = PreprocessingStep() + } +} \ 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/construction/processing/TaskPlanner.kt similarity index 92% rename from common/src/main/kotlin/com/lambda/interaction/processing/TaskPlanner.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/processing/TaskPlanner.kt index 3f9529112..e8da4e1fc 100644 --- a/common/src/main/kotlin/com/lambda/interaction/processing/TaskPlanner.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/TaskPlanner.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.interaction.processing +package com.lambda.interaction.construction.processing object TaskPlanner { -} +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/AxisPreprocessor.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/AxisPreprocessor.kt new file mode 100644 index 000000000..0ff14d216 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/AxisPreprocessor.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing.processors + +import com.lambda.interaction.construction.processing.PlacementProcessor +import com.lambda.interaction.construction.processing.PreprocessingStep +import net.minecraft.block.BlockState +import net.minecraft.state.property.Properties +import net.minecraft.util.math.Direction + +object AxisPreprocessor : PlacementProcessor() { + override fun acceptState(state: BlockState) = + state.getOrEmpty(Properties.AXIS).isPresent + + override fun preProcess(state: BlockState): PreprocessingStep { + val axis = state.getOrEmpty(Properties.AXIS).get() + return PreprocessingStep( + sides = Direction.entries.filter { it.axis == axis }.toSet() + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/BlockFaceProcessor.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/BlockFaceProcessor.kt new file mode 100644 index 000000000..359709d5e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/BlockFaceProcessor.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing.processors + +import com.lambda.interaction.construction.processing.PlacementProcessor +import com.lambda.interaction.construction.processing.PreprocessingStep +import net.minecraft.block.BlockState +import net.minecraft.block.enums.BlockFace +import net.minecraft.state.property.Properties +import net.minecraft.util.math.Direction +import kotlin.jvm.optionals.getOrNull + +object BlockFaceProcessor : PlacementProcessor() { + override fun acceptState(state: BlockState) = + state.getOrEmpty(Properties.BLOCK_FACE).isPresent + + override fun preProcess(state: BlockState): PreprocessingStep { + val property = state.getOrEmpty(Properties.BLOCK_FACE).getOrNull() ?: return PreprocessingStep() + val sides = when (property) { + BlockFace.FLOOR -> setOf(Direction.DOWN) + BlockFace.CEILING -> setOf(Direction.UP) + BlockFace.WALL -> Direction.Type.HORIZONTAL.stream().toList().toSet() + } + return PreprocessingStep(sides = sides) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/FacingProcessor.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/FacingProcessor.kt new file mode 100644 index 000000000..62e791d4f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/FacingProcessor.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing.processors + +import com.lambda.interaction.construction.processing.PlacementProcessor +import com.lambda.interaction.construction.processing.PreprocessingStep +import net.minecraft.block.BlockState +import net.minecraft.state.property.Properties + +object FacingProcessor : PlacementProcessor() { + override fun acceptState(state: BlockState) = + state.getOrEmpty(Properties.FACING).isPresent + + override fun preProcess(state: BlockState): PreprocessingStep { + // Needs two sets of blocks: native and opposite! + return PreprocessingStep( + sides = setOf(state.getOrEmpty(Properties.FACING).get()) + ) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/SlabProcessor.kt b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/SlabProcessor.kt new file mode 100644 index 000000000..ac5794690 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/processing/processors/SlabProcessor.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.processing.processors + +import com.lambda.interaction.construction.processing.PlacementProcessor +import com.lambda.interaction.construction.processing.PreprocessingStep +import com.lambda.interaction.construction.verify.ScanMode +import com.lambda.interaction.construction.verify.SurfaceScan +import net.minecraft.block.BlockState +import net.minecraft.block.SlabBlock +import net.minecraft.block.enums.SlabType +import net.minecraft.state.property.Properties +import net.minecraft.util.math.Direction + +object SlabProcessor : PlacementProcessor() { + override fun acceptState(state: BlockState) = state.block is SlabBlock + + override fun preProcess(state: BlockState): PreprocessingStep { + val slab = state.getOrEmpty(Properties.SLAB_TYPE).get() + + val surfaceScan = when (slab) { + SlabType.BOTTOM -> SurfaceScan( + ScanMode.LESSER_HALF, Direction.Axis.Y + ) + SlabType.TOP -> SurfaceScan( + ScanMode.GREATER_HALF, Direction.Axis.Y + ) + SlabType.DOUBLE -> SurfaceScan( + ScanMode.FULL, Direction.Axis.Y + ) + } + + return PreprocessingStep(surfaceScan) + } +} \ 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 index 526a96747..182ec9623 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt @@ -21,12 +21,13 @@ import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted import com.lambda.context.SafeContext 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.container.ContainerManager.findBestAvailableTool +import com.lambda.interaction.material.container.ContainerManager.transfer +import com.lambda.interaction.material.container.MaterialContainer 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.tasks.BreakBlock.Companion.breakBlock +import com.lambda.interaction.material.container.containers.MainHandContainer +import com.lambda.task.tasks.BreakBlock import net.minecraft.block.BlockState import net.minecraft.item.Item import net.minecraft.util.math.BlockPos @@ -42,20 +43,17 @@ sealed class BreakResult : BuildResult() { data class Break( override val blockPos: BlockPos, val context: BreakContext - ) : Drawable, BreakResult() { + ) : Drawable, Resolvable, BreakResult() { override val rank = Rank.BREAK_SUCCESS private val color = Color(222, 0, 0, 100) var collectDrop = false + override val pausesParent get() = collectDrop - override fun SafeContext.onStart() { - breakBlock(context, collectDrop = collectDrop).onSuccess { _, _ -> - success(Unit) - }.start(this@Break) - } + override fun resolve() = BreakBlock(context, collectDrop) override fun SafeContext.buildRenderer() { - withPos(context.resultingPos, color, context.result.side) + withPos(context.expectedPos, color, context.result.side) } override fun compareTo(other: ComparableResult): Int { @@ -78,10 +76,6 @@ sealed class BreakResult : BuildResult() { override val rank = Rank.BREAK_NOT_EXPOSED private val color = Color(46, 0, 0, 30) - override fun SafeContext.onStart() { - failure("Block is not exposed to air.") - } - override fun SafeContext.buildRenderer() { withPos(blockPos, color, side) } @@ -103,24 +97,20 @@ sealed class BreakResult : BuildResult() { override val blockPos: BlockPos, val blockState: BlockState, val badItem: Item - ) : Drawable, BreakResult() { + ) : Drawable, Resolvable, BreakResult() { override val rank = Rank.BREAK_ITEM_CANT_MINE private val color = Color(255, 0, 0, 100) override val pausesParent get() = true - override fun SafeContext.onStart() { + override fun resolve() = findBestAvailableTool(blockState) ?.select() ?.transfer(MainHandContainer) - ?.onSuccess { _, _ -> - success(Unit) - }?.start(this@ItemCantMine) ?: run { - selectStack { + ?: selectStack { isItem(badItem).not() - }.transfer(MainHandContainer)?.start(this@ItemCantMine) ?: failure("No item found or space") - } - } + }.transfer(MainHandContainer) + ?: MaterialContainer.Nothing("Couldn't find a tool for ${blockState.block.name.string} with $badItem in main hand.") override fun SafeContext.buildRenderer() { withPos(blockPos, color) 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 index 5a3403b8a..8954f1699 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt @@ -19,15 +19,14 @@ 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.context.SafeContext import com.lambda.interaction.construction.context.BuildContext -import com.lambda.interaction.material.ContainerManager.transfer +import com.lambda.interaction.material.container.ContainerManager.transfer +import com.lambda.interaction.material.container.MaterialContainer import com.lambda.interaction.material.StackSelection.Companion.select -import com.lambda.interaction.material.container.MainHandContainer -import com.lambda.task.Task -import com.lambda.task.Task.Companion.failTask +import com.lambda.interaction.material.container.containers.MainHandContainer import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Nameable import net.minecraft.block.BlockState import net.minecraft.item.Item import net.minecraft.item.ItemStack @@ -37,16 +36,19 @@ import net.minecraft.util.math.Direction import net.minecraft.util.math.Vec3d import java.awt.Color -abstract class BuildResult : ComparableResult, Task() { +abstract class BuildResult : ComparableResult, Nameable { abstract val blockPos: BlockPos open val pausesParent = false + override val name: String get() = "${this::class.simpleName} at ${blockPos.toShortString()}" /** * The build action is done. */ data class Done( - override val blockPos: BlockPos, + override val blockPos: BlockPos ) : BuildResult() { + override val name: String + get() = "Build at $blockPos is done." override val rank = Rank.DONE } @@ -54,8 +56,10 @@ abstract class BuildResult : ComparableResult, Task() { * The build action is ignored. */ data class Ignored( - override val blockPos: BlockPos, + override val blockPos: BlockPos ) : BuildResult() { + override val name: String + get() = "Build at $blockPos is ignored." override val rank = Rank.IGNORED } @@ -66,6 +70,7 @@ abstract class BuildResult : ComparableResult, Task() { data class ChunkNotLoaded( override val blockPos: BlockPos ) : Navigable, Drawable, BuildResult() { + override val name: String get() = "Chunk at $blockPos is not loaded." override val rank = Rank.CHUNK_NOT_LOADED private val color = Color(252, 165, 3, 100) @@ -90,6 +95,7 @@ abstract class BuildResult : ComparableResult, Task() { data class Restricted( override val blockPos: BlockPos ) : Drawable, BuildResult() { + override val name: String get() = "Restricted at $blockPos." override val rank = Rank.BREAK_RESTRICTED private val color = Color(255, 0, 0, 100) @@ -107,6 +113,7 @@ abstract class BuildResult : ComparableResult, Task() { override val blockPos: BlockPos, val blockState: BlockState ) : Drawable, BuildResult() { + override val name: String get() = "No permission at $blockPos." override val rank get() = Rank.BREAK_NO_PERMISSION private val color = Color(255, 0, 0, 100) @@ -122,6 +129,7 @@ abstract class BuildResult : ComparableResult, Task() { data class OutOfWorld( override val blockPos: BlockPos ) : Drawable, BuildResult() { + override val name: String get() = "$blockPos is out of the world." override val rank = Rank.OUT_OF_WORLD private val color = Color(3, 148, 252, 100) @@ -139,6 +147,7 @@ abstract class BuildResult : ComparableResult, Task() { override val blockPos: BlockPos, val blockState: BlockState ) : Drawable, BuildResult() { + override val name: String get() = "Unbreakable at $blockPos." override val rank = Rank.UNBREAKABLE private val color = Color(11, 11, 11, 100) @@ -158,6 +167,7 @@ abstract class BuildResult : ComparableResult, Task() { val side: Direction, val distance: Double ) : Drawable, BuildResult() { + override val name: String get() = "Not visible at $blockPos." override val rank = Rank.NOT_VISIBLE private val color = Color(46, 0, 0, 80) @@ -180,20 +190,17 @@ abstract class BuildResult : ComparableResult, Task() { data class WrongItem( override val blockPos: BlockPos, val context: BuildContext, - val neededItem: Item - ) : Drawable, BuildResult() { + val neededItem: Item, + val currentItem: ItemStack, + ) : Drawable, Resolvable, BuildResult() { + override val name: String get() = "Wrong item ($currentItem) for ${blockPos.toShortString()} need ${neededItem.name.string}" override val rank = Rank.WRONG_ITEM private val color = Color(3, 252, 169, 100) override val pausesParent get() = true - override fun SafeContext.onStart() { - neededItem.select() - .transfer(MainHandContainer) - ?.onSuccess { _, _ -> - success(Unit) - }?.start(this@WrongItem) ?: failure("Item ${neededItem.name.string} not found") - } + override fun resolve() = neededItem.select() + .transfer(MainHandContainer) ?: MaterialContainer.Nothing() override fun SafeContext.buildRenderer() { if (blockPos.blockState(world).isAir) { @@ -220,19 +227,15 @@ abstract class BuildResult : ComparableResult, Task() { override val blockPos: BlockPos, val context: BuildContext, val neededStack: ItemStack - ) : Drawable, BuildResult() { + ) : Drawable, Resolvable, BuildResult() { + override val name: String get() = "Wrong stack for $blockPos need $neededStack." override val rank = Rank.WRONG_ITEM private val color = Color(3, 252, 169, 100) override val pausesParent get() = true - override fun SafeContext.onStart() { - neededStack.select() - .transfer(MainHandContainer) - ?.onSuccess { _, _ -> - success(Unit) - }?.start(this@WrongStack) ?: failTask("Stack ${neededStack.name.string} not found") - } + override fun resolve() = + neededStack.select().transfer(MainHandContainer) ?: MaterialContainer.Nothing() override fun SafeContext.buildRenderer() { if (blockPos.blockState(world).isAir) { @@ -253,26 +256,23 @@ abstract class BuildResult : ComparableResult, Task() { /** * 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. + * @param pov The point of view of the player. + * @param misses The points that are out of reach. */ data class OutOfReach( override val blockPos: BlockPos, - val startVec: Vec3d, - val hitVec: Vec3d, - val reach: Double, - val side: Direction, + val pov: Vec3d, + val misses: Set ) : Navigable, Drawable, BuildResult() { + override val name: String get() = "Out of reach at $blockPos." override val rank = Rank.OUT_OF_REACH private val color = Color(252, 3, 207, 100) val distance: Double by lazy { - startVec.distanceTo(hitVec) + misses.minOfOrNull { pov.distanceTo(it) } ?: 0.0 } - override val goal = GoalNear(blockPos, 2) + override val goal = GoalNear(blockPos, 3) override fun SafeContext.buildRenderer() { withPos(blockPos, color) 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 index b7f114601..5cd46bfe0 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt @@ -21,8 +21,9 @@ import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted import com.lambda.context.SafeContext import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.task.Task import com.lambda.task.tasks.BuildTask.Companion.breakBlock -import com.lambda.task.tasks.PlaceBlock.Companion.placeBlock +import com.lambda.task.tasks.PlaceBlock import net.minecraft.block.BlockState import net.minecraft.item.ItemPlacementContext import net.minecraft.item.ItemStack @@ -44,23 +45,8 @@ sealed class PlaceResult : BuildResult() { data class Place( override val blockPos: BlockPos, val context: PlaceContext - ) : Drawable, PlaceResult() { + ) : PlaceResult() { override val rank = Rank.PLACE_SUCCESS - private val color = Color(35, 188, 254, 100) - - override fun SafeContext.onStart() { - placeBlock(context).onSuccess { _, _ -> - success(Unit) - }.start(this@Place) - } - - override fun SafeContext.buildRenderer() { - val hitPos = context.result.blockPos - withPos(hitPos, color, context.result.side) - - val light = Color(35, 188, 254, 20) - withState(context.expectedState, context.resultingPos, light) - } override fun compareTo(other: ComparableResult): Int { return when (other) { @@ -71,15 +57,21 @@ sealed class PlaceResult : BuildResult() { } /** - * 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. + * Represents a placement result where the block placement does not meet integrity expectations. + * + * This class is used to provide details about a block placement issue in which the actual block + * placed does not match the expected state, or additional integrity conditions are not met. + * + * @property blockPos The position of the block being inspected or placed. + * @property expected The expected state of the block. + * @property simulated The context of the item placement simulation. + * @property actual The expected */ data class NoIntegrity( override val blockPos: BlockPos, val expected: BlockState, - val simulated: ItemPlacementContext + val simulated: ItemPlacementContext, + val actual: BlockState? = null ) : Drawable, PlaceResult() { override val rank = Rank.PLACE_NO_INTEGRITY private val color = Color(252, 3, 3, 100) @@ -89,6 +81,11 @@ sealed class PlaceResult : BuildResult() { } } + /** + * Represents a scenario where block placement is obstructed by a player. + * + * @property blockPos The position of the block that was attempted to be placed. + */ data class BlockedByPlayer( override val blockPos: BlockPos ) : Navigable, PlaceResult() { @@ -98,25 +95,25 @@ sealed class PlaceResult : BuildResult() { } /** - * The placement configuration cannot replace the block at the target position. - * @param simulated The simulated placement configuration. + * Represents a result indicating that a block cannot be replaced during a placement operation. + * + * @property blockPos The position of the block that cannot be replaced. + * @property simulated The context of the item placement simulation. */ data class CantReplace( override val blockPos: BlockPos, val simulated: ItemPlacementContext - ) : PlaceResult() { + ) : Resolvable, PlaceResult() { override val rank = Rank.PLACE_CANT_REPLACE - override fun SafeContext.onStart() { - breakBlock(blockPos).onSuccess { _, _ -> - success(Unit) - }.start(this@CantReplace) - } + override fun resolve() = breakBlock(blockPos) } /** - * The placement configuration exceeds the maximum scaffold distance. - * @param simulated The simulated placement configuration. + * Represents a placement result indicating that the scaffolding placement has exceeded the allowed limits. + * + * @property blockPos The position of the block where the placement attempt occurred. + * @property simulated The context of the simulated item placement attempt. */ data class ScaffoldExceeded( override val blockPos: BlockPos, @@ -126,7 +123,11 @@ sealed class PlaceResult : BuildResult() { } /** - * The interaction with the block is restricted due to the feature being disabled. + * Represents a result where a block placement operation was prevented because + * the relevant block feature is disabled. + * + * @property blockPos The position of the block that could not be placed. + * @property itemStack The item stack associated with the attempted placement. */ data class BlockFeatureDisabled( override val blockPos: BlockPos, @@ -136,7 +137,24 @@ sealed class PlaceResult : BuildResult() { } /** - * The player has no permission to interact with the block. Or the stack cannot be used on the block. + * Represents a result state where the placement or manipulation of a block resulted in an unexpected position. + * + * @property blockPos The intended position of the block. + * @property actualPos The actual position of the block, which differs from the intended position. + */ + data class UnexpectedPosition( + override val blockPos: BlockPos, + val actualPos: BlockPos + ) : PlaceResult() { + override val rank = Rank.UNEXPECTED_POSITION + } + + /** + * Represents a result indicating an illegal usage during a placement operation. + * E.g., the player can't modify the world or the block cannot be placed against the surface. + * + * @property blockPos The position of the block associated with the illegal usage result. + * @property rank The ranking of this result, which is always `PLACE_ILLEGAL_USAGE`. */ data class IllegalUsage( override val blockPos: BlockPos @@ -145,8 +163,10 @@ sealed class PlaceResult : BuildResult() { } /** - * The item stack is not a block item. - * This will be resolved by analyzing interaction with non-block items. + * Represents the result of a place operation where the provided item does not match the expected item block type. + * + * @property blockPos The position of the block where the operation was attempted. + * @property itemStack The item stack that was checked during the place operation. */ data class NotItemBlock( override val blockPos: BlockPos, 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 index 0d30ca5a0..da6b5a599 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt @@ -42,6 +42,7 @@ enum class Rank { BREAK_NO_PERMISSION, PLACE_SCAFFOLD_EXCEEDED, PLACE_BLOCK_FEATURE_DISABLED, + UNEXPECTED_POSITION, PLACE_ILLEGAL_USAGE, // not an issue @@ -49,5 +50,5 @@ enum class Rank { IGNORED; val solvable: Boolean - get() = ordinal < OUT_OF_WORLD.ordinal + get() = ordinal < PLACE_NOT_ITEM_BLOCK.ordinal } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt b/common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt similarity index 84% rename from common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt index 88d867c58..ac28db61c 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferStep.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/result/Resolvable.kt @@ -15,14 +15,10 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.transfer +package com.lambda.interaction.construction.result import com.lambda.task.Task -abstract class TransferStep : Task() { - - - companion object { - - } -} +interface Resolvable { + fun resolve(): Task<*> +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildGoal.kt b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildGoal.kt index 23e4f1c1e..b1259b228 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildGoal.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildGoal.kt @@ -18,21 +18,16 @@ package com.lambda.interaction.construction.simulation import baritone.api.pathing.goals.Goal -import com.lambda.util.Communication.info import com.lambda.util.world.fastVectorOf class BuildGoal(private val sim: Simulation) : Goal { - override fun isInGoal(x: Int, y: Int, z: Int): Boolean { - val vec = fastVectorOf(x, y, z) -// info("Checking goal at $x, $y, $z") - val simu = sim.simulate(vec) - return simu.any { it.rank.ordinal < 4 } - } + override fun isInGoal(x: Int, y: Int, z: Int) = + sim.simulate(fastVectorOf(x, y, z)) + .any { it.rank.ordinal < 4 } override fun heuristic(x: Int, y: Int, z: Int): Double { - val bestRank = sim.simulate(fastVectorOf(x, y, z)).minOrNull()?.rank?.ordinal ?: 10000 - val score = 1 / (bestRank.toDouble() + 1) -// info("Calculating heuristic at $x, $y, $z with score $score") - return score + val bestRank = sim.simulate(fastVectorOf(x, y, z)) + .minOrNull()?.rank?.ordinal ?: 100000 + return 1 / (bestRank.toDouble() + 1) } } 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 index 91c50ef47..03bed4002 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt @@ -19,26 +19,32 @@ 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.blueprint.Blueprint import com.lambda.interaction.construction.context.BreakContext import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.interaction.construction.processing.ProcessorRegistry.findProcessorForState 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.material.container.ContainerManager.findBestAvailableTool +import com.lambda.interaction.rotation.Rotation.Companion.rotation import com.lambda.interaction.rotation.Rotation.Companion.rotationTo import com.lambda.interaction.rotation.RotationContext +import com.lambda.interaction.visibilty.VisibilityChecker.getVisibleSurfaces import com.lambda.interaction.visibilty.VisibilityChecker.optimum -import com.lambda.interaction.visibilty.VisibilityChecker.scanVisibleSurfaces -import com.lambda.module.modules.client.TaskFlow +import com.lambda.interaction.visibilty.VisibilityChecker.scanSurfaces +import com.lambda.interaction.visibilty.VisibilityChecker.visibleSides +import com.lambda.module.modules.client.TaskFlowModule 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.Communication.warn import com.lambda.util.item.ItemStackUtils.equal import com.lambda.util.math.VecUtils.distSq +import com.lambda.util.player.copyPlayer import com.lambda.util.world.raycast.RayCastUtils.blockResult import net.minecraft.block.OperatorBlock import net.minecraft.block.pattern.CachedBlockPosition @@ -46,6 +52,7 @@ import net.minecraft.item.BlockItem import net.minecraft.item.ItemPlacementContext import net.minecraft.item.ItemUsageContext import net.minecraft.registry.RegistryKeys +import net.minecraft.state.property.Properties import net.minecraft.util.Hand import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.hit.HitResult @@ -53,13 +60,14 @@ 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 kotlin.jvm.optionals.getOrNull import kotlin.math.pow object BuildSimulator { - fun Blueprint.simulate(eye: Vec3d, reach: Double = TaskFlow.interact.reach) = + fun Blueprint.simulate(eye: Vec3d, reach: Double = TaskFlowModule.interact.reach) = runSafe { structure.entries.flatMap { (pos, target) -> - checkRequirements(pos, target, reach)?.let { + checkRequirements(pos, target)?.let { return@flatMap setOf(it) } checkPlaceResults(pos, target, eye, reach).let { @@ -70,11 +78,12 @@ object BuildSimulator { if (it.isEmpty()) return@let return@flatMap it } + warn("Nothing matched $pos $target") emptySet() }.toSet() } ?: emptySet() - private fun SafeContext.checkRequirements(pos: BlockPos, target: TargetState, reach: Double): BuildResult? { + private fun SafeContext.checkRequirements(pos: BlockPos, target: TargetState): BuildResult? { /* the chunk is not loaded */ if (!world.isChunkLoaded(pos)) { return BuildResult.ChunkNotLoaded(pos) @@ -88,7 +97,7 @@ object BuildSimulator { } /* block should be ignored */ - if (state.block in TaskFlow.ignoredBlocks) { + if (state.block in TaskFlowModule.build.ignoredBlocks && target.type == TargetState.Type.AIR) { return BuildResult.Ignored(pos) } @@ -108,7 +117,7 @@ object BuildSimulator { } /* block is unbreakable, so it cant be broken or replaced */ - if (state.getHardness(world, pos) < 0) { + if (state.getHardness(world, pos) < 0 && !player.isCreative) { return BuildResult.Unbreakable(pos, state) } @@ -122,58 +131,62 @@ object BuildSimulator { reach: Double ): Set { val acc = mutableSetOf() + val targetPosState = pos.blockState(world) - if (target is TargetState.Air || !pos.blockState(world).isReplaceable) return acc + if (target.isAir() || !targetPosState.isReplaceable) return acc - val interact = TaskFlow.interact - val rotation = TaskFlow.rotation + val interact = TaskFlowModule.interact + val rotation = TaskFlowModule.rotation - Direction.entries.forEach { neighbor -> - val hitPos = pos.offset(neighbor) + val preprocessing = target.findProcessorForState() + + preprocessing.sides.forEach { neighbor -> + val hitPos = if (targetPosState.isAir) pos.offset(neighbor) else pos 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) > reach + 1 }) { - acc.add(BuildResult.OutOfReach(pos, eye, hitPos.vecOf(hitSide), reach, hitSide)) - return@forEach - } - val verify: HitResult.() -> Boolean = { blockResult?.blockPos == hitPos && blockResult?.side == hitSide } val validHits = mutableMapOf() + val misses = mutableSetOf() val reachSq = reach.pow(2) boxes.forEach { box -> - val res = if (TaskFlow.interact.useRayCast) interact.resolution else 2 - scanVisibleSurfaces(eye, box, setOf(hitSide), res) { side, vec -> + val res = if (TaskFlowModule.interact.useRayCast) interact.resolution else 4 + val sides = if (TaskFlowModule.interact.visibilityCheck) { + box.getVisibleSurfaces(eye).intersect(setOf(hitSide)) + } else { + Direction.entries.toSet() + } + + scanSurfaces(box, sides, res, preprocessing.surfaceScan) { side, vec -> if (eye distSq vec > reachSq) { - return@scanVisibleSurfaces + misses.add(vec) + return@scanSurfaces } - validHits[vec] = if (TaskFlow.interact.useRayCast) { + validHits[vec] = if (TaskFlowModule.interact.useRayCast && TaskFlowModule.interact.visibilityCheck) { val cast = eye.rotationTo(vec) - .rayCast(reach, eye) ?: return@scanVisibleSurfaces - if (!cast.verify()) return@scanVisibleSurfaces + .rayCast(reach, eye) ?: return@scanSurfaces + if (!cast.verify()) return@scanSurfaces cast } else { - BlockHitResult( - vec, - side, - hitPos, - false - ) + BlockHitResult(vec, side, hitPos, false) } - } } if (validHits.isEmpty()) { + if (misses.isNotEmpty()) { + acc.add(BuildResult.OutOfReach(pos, eye, misses)) + return@forEach + } + acc.add(BuildResult.NotVisible(pos, hitPos, hitSide, eye.distanceTo(hitPos.vecOf(hitSide)))) return@forEach } @@ -186,9 +199,15 @@ object BuildSimulator { }?.let { rotation -> val optimalStack = target.getStack(world, pos) + // ToDo: For each hand and sneak or not? + val fakePlayer = copyPlayer(player).apply { + setPos(eye.x, eye.y - standingEyeHeight, eye.z) + this.rotation = rotation.rotation + } + val usageContext = ItemUsageContext( - player, - Hand.MAIN_HAND, // ToDo: notice that the hand may have a different item stack and simulation will be wrong + fakePlayer, + Hand.MAIN_HAND, rotation.hitResult?.blockResult, ) val cachePos = CachedBlockPosition( @@ -207,6 +226,11 @@ object BuildSimulator { var context = ItemPlacementContext(usageContext) + if (context.blockPos != pos) { + acc.add(PlaceResult.UnexpectedPosition(pos, context.blockPos)) + return@forEach + } + if (!optimalStack.item.isEnabled(world.enabledFeatures)) { acc.add(PlaceResult.BlockFeatureDisabled(pos, optimalStack)) return@forEach @@ -231,12 +255,14 @@ object BuildSimulator { } val resultState = blockItem.getPlacementState(context) ?: run { - acc.add(PlaceResult.BlockedByPlayer(pos)) +// acc.add(PlaceResult.BlockedByPlayer(pos)) return@forEach } if (!target.matches(resultState, pos, world)) { - acc.add(PlaceResult.NoIntegrity(pos, resultState, context)) + acc.add(PlaceResult.NoIntegrity( + pos, resultState, context, (target as? TargetState.State)?.blockState) + ) return@forEach } @@ -244,6 +270,11 @@ object BuildSimulator { val hitBlock = blockHit.blockPos.blockState(world).block val shouldSneak = hitBlock in BlockUtils.interactionBlacklist + val primeDirection = (target as? TargetState.State) + ?.blockState + ?.getOrEmpty(Properties.HORIZONTAL_FACING) + ?.getOrNull() + val placeContext = PlaceContext( eye, blockHit, @@ -252,9 +283,11 @@ object BuildSimulator { resultState, blockHit.blockPos.blockState(world), Hand.MAIN_HAND, + context.blockPos, target, shouldSneak, - false + false, + primeDirection ) val currentHandStack = player.getStackInHand(Hand.MAIN_HAND) @@ -264,7 +297,7 @@ object BuildSimulator { } if (optimalStack.item != currentHandStack.item) { - acc.add(BuildResult.WrongItem(pos, placeContext, optimalStack.item)) + acc.add(BuildResult.WrongItem(pos, placeContext, optimalStack.item, currentHandStack)) return@forEach } @@ -275,12 +308,16 @@ object BuildSimulator { return acc } - private fun SafeContext.checkBreakResults(pos: BlockPos, eye: Vec3d, reach: Double): Set { + private fun SafeContext.checkBreakResults( + pos: BlockPos, + eye: Vec3d, + reach: Double + ): 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) { + if (TaskFlowModule.build.breakWeakBlocks && state.block.hardness == 0f && !state.isAir) { acc.add(BuildResult.Ignored(pos)) return acc } @@ -290,6 +327,8 @@ object BuildSimulator { 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 + val belowSupport = support.down().blockState(world) + if (belowSupport.isSolidSurface(world, support, player, Direction.UP)) return@let acc.add(BreakResult.PlayerOnTop(pos, state)) return acc } @@ -325,22 +364,17 @@ object BuildSimulator { return acc } - val interact = TaskFlow.interact - val rotation = TaskFlow.rotation + val interact = TaskFlowModule.interact + val rotation = TaskFlowModule.rotation val currentRotation = RotationManager.currentRotation val currentCast = currentRotation.rayCast(reach, eye) val voxelShape = state.getOutlineShape(world, pos) voxelShape.getClosestPointTo(eye).ifPresent { - // ToDo: Use closest point of shape + // ToDo: Use closest point of shape of only visible faces } val boxes = voxelShape.boundingBoxes.map { it.offset(pos) } - if (boxes.all { it.center.distanceTo(eye) > reach + 1 }) { - acc.add(BuildResult.OutOfReach(pos, eye, pos.toCenterPos(), 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) }) { @@ -360,32 +394,36 @@ object BuildSimulator { } val validHits = mutableMapOf() + val misses = mutableSetOf() val reachSq = reach.pow(2) boxes.forEach { box -> - val res = if (TaskFlow.interact.useRayCast) interact.resolution else 2 - scanVisibleSurfaces(eye, box, emptySet(), res) { side, vec -> + val res = if (TaskFlowModule.interact.useRayCast) interact.resolution else 2 + val sides = visibleSides(box, eye, TaskFlowModule.interact) + scanSurfaces(box, sides, res) { side, vec -> if (eye distSq vec > reachSq) { - return@scanVisibleSurfaces + misses.add(vec) + return@scanSurfaces } - validHits[vec] = if (TaskFlow.interact.useRayCast) { + validHits[vec] = if (TaskFlowModule.interact.useRayCast && TaskFlowModule.interact.visibilityCheck) { val cast = eye.rotationTo(vec) - .rayCast(reach, eye) ?: return@scanVisibleSurfaces - if (!cast.verify()) return@scanVisibleSurfaces + .rayCast(reach, eye) ?: return@scanSurfaces + if (!cast.verify()) return@scanSurfaces cast } else { - BlockHitResult( - vec, - side, - pos, - false - ) + BlockHitResult(vec, side, pos, false) } } } + if (validHits.isEmpty()) { + // ToDo: If we can only mine exposed surfaces we need to add not visible result here + acc.add(BuildResult.OutOfReach(pos, eye, misses)) + return acc + } + validHits.keys.optimum?.let { optimum -> validHits.minByOrNull { optimum distSq it.key }?.let { closest -> val optimumRotation = eye.rotationTo(closest.key) @@ -413,14 +451,13 @@ object BuildSimulator { acc.add(BreakResult.Break(pos, breakContext)) return acc } ?: run { - acc.add(BuildResult.WrongItem(pos, breakContext, bestTool)) + acc.add(BuildResult.WrongItem(pos, breakContext, bestTool, player.activeItem)) return acc } } acc.add(BreakResult.Break(pos, breakContext)) } - return acc } } diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt index 383747a94..f8c05bb27 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt @@ -18,57 +18,35 @@ package com.lambda.interaction.construction.simulation import com.lambda.context.SafeContext -import com.lambda.interaction.construction.Blueprint +import com.lambda.interaction.construction.blueprint.Blueprint import com.lambda.interaction.construction.result.BuildResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.threading.runSafe -import com.lambda.util.Communication.info +import com.lambda.util.BlockUtils.blockState import com.lambda.util.world.FastVector import com.lambda.util.world.toBlockPos -import com.lambda.util.world.toFastVec import com.lambda.util.world.toVec3d import net.minecraft.client.network.ClientPlayerEntity -import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box +import net.minecraft.util.math.Direction import net.minecraft.util.math.Vec3d data class Simulation(val blueprint: Blueprint) { private val cache: MutableMap> = mutableMapOf() - private fun FastVector.toView(): Vec3d = toVec3d().add(0.5, 0.62, 0.5) -// private lateinit var player: ClientPlayerEntity - -// init { -// runSafe { -// this@Simulation.player = player -// BlockPos.iterateOutwards(player.blockPos, 5, 5, 5).forEach { pos -> -// val vec = pos.toFastVec() -// info("Preloading simulation at $vec") -// simulate(vec) -// } -// } -// } - -// val best: FastVector? get() = cache.filter { (_, results) -> -// results.isNotEmpty() && results.any { it.rank.ordinal < 4 } -// }.keys.minByOrNull { it.toVec3d().distanceTo(player.pos) } - -// fun best() = cache.filter { (_, results) -> -// results.isNotEmpty() && results.any { it.rank.ordinal < 3 } -// }.keys - - fun simulate(pos: FastVector): Set { -// runSafe { -// if (!playerFitsIn(Vec3d.ofBottomCenter(pos.toBlockPos()))) return emptySet() -// } -// return blueprint.simulate(pos.toView()).also { cache[pos] = it } -// return cache.computeIfAbsent(pos) { -//// runSafe { -//// if (!playerFitsIn(Vec3d.ofBottomCenter(pos.toBlockPos()))) return@computeIfAbsent emptySet() -//// } -// blueprint.simulate(pos.toView()) -// } - return blueprint.simulate(pos.toView(), reach = 3.5) - } + private fun FastVector.toView(): Vec3d = toVec3d().add(0.5, ClientPlayerEntity.DEFAULT_EYE_HEIGHT.toDouble(), 0.5) + + fun simulate(pos: FastVector) = + cache.getOrPut(pos) { + val view = pos.toView() + runSafe { + if (blueprint.isOutOfBounds(view) && blueprint.getClosestPointTo(view).distanceTo(view) > 10.0) return@getOrPut emptySet() + val blockPos = pos.toBlockPos() + if (!playerFitsIn(Vec3d.ofBottomCenter(blockPos))) return@getOrPut emptySet() + if (!blockPos.down().blockState(world).isSideSolidFullSquare(world, blockPos, Direction.UP)) return@getOrPut emptySet() + } + blueprint.simulate(view, reach = TaskFlowModule.interact.reach - 1) + } private fun SafeContext.playerFitsIn(pos: Vec3d): Boolean { val pBox = player.boundingBox diff --git a/common/src/main/kotlin/com/lambda/task/RootTask.kt b/common/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt similarity index 74% rename from common/src/main/kotlin/com/lambda/task/RootTask.kt rename to common/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt index 9f1b7b9c8..44b190fdc 100644 --- a/common/src/main/kotlin/com/lambda/task/RootTask.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt @@ -15,15 +15,8 @@ * along with this program. If not, see . */ -package com.lambda.task +package com.lambda.interaction.construction.verify -object RootTask : Task() { - init { - name = "RootTask" - } - - fun addInfo(debugText: MutableList) { - debugText.add("") - debugText.addAll(info.string.split("\n")) - } -} +enum class ScanMode { + GREATER_HALF, LESSER_HALF, FULL +} \ 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 index 6f5e416d8..af2aaec11 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt @@ -25,4 +25,5 @@ import net.minecraft.util.math.BlockPos interface StateMatcher { fun matches(state: BlockState, pos: BlockPos, world: ClientWorld): Boolean fun getStack(world: ClientWorld, pos: BlockPos): ItemStack + fun isAir(): Boolean } diff --git a/common/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt b/common/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt new file mode 100644 index 000000000..78586b7f7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.verify + +import net.minecraft.util.math.Direction + +data class SurfaceScan( + val mode: ScanMode, + val axis: Direction.Axis, +) { + companion object { + val DEFAULT = SurfaceScan(ScanMode.FULL, Direction.Axis.Y) + } + +} \ 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 index 78a480775..22982d5e2 100644 --- a/common/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt +++ b/common/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt @@ -17,9 +17,10 @@ package com.lambda.interaction.construction.verify -import com.lambda.interaction.material.ContainerManager.findDisposable -import com.lambda.module.modules.client.TaskFlow +import com.lambda.interaction.material.container.ContainerManager.findDisposable +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.util.BlockUtils.blockState +import com.lambda.util.StringUtils.capitalize import com.lambda.util.item.ItemUtils.block import net.minecraft.block.BlockState import net.minecraft.client.world.ClientWorld @@ -28,53 +29,82 @@ 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() { +sealed class TargetState(val type: Type) : StateMatcher { + + enum class Type { + AIR, SOLID, SUPPORT, STATE, BLOCK, STACK + } + + data object Air : TargetState(Type.AIR) { + override fun toString() = "Air" + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = state.isAir override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = ItemStack.EMPTY + + override fun isAir() = true } - data object Solid : TargetState() { + data object Solid : TargetState(Type.SOLID) { + override fun toString() = "Solid" + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = state.isSolidBlock(world, pos) override fun getStack(world: ClientWorld, pos: BlockPos) = findDisposable()?.stacks?.firstOrNull { - it.item.block in TaskFlow.disposables + it.item.block in TaskFlowModule.inventory.disposables } ?: ItemStack(Items.NETHERRACK) + + override fun isAir() = false } - data class Support(val direction: Direction) : TargetState() { + data class Support(val direction: Direction) : TargetState(Type.SUPPORT) { + override fun toString() = "Support for ${direction.name}" + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = pos.offset(direction).blockState(world).isSolidBlock(world, pos.offset(direction)) || state.isSolidBlock(world, pos) override fun getStack(world: ClientWorld, pos: BlockPos) = findDisposable()?.stacks?.firstOrNull { - it.item.block in TaskFlow.disposables + it.item.block in TaskFlowModule.inventory.disposables } ?: ItemStack(Items.NETHERRACK) + + override fun isAir() = false } - data class State(val blockState: BlockState) : TargetState() { - override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = - state == blockState + data class State(val blockState: BlockState) : TargetState(Type.STATE) { + override fun toString() = "State of $blockState" + override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = + state.block == blockState.block && state.properties.all { + /*it in TaskFlowModule.defaultIgnoreTags ||*/ state[it] == blockState[it] + } override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = blockState.block.getPickStack(world, pos, blockState) + + override fun isAir() = blockState.isAir } - data class Block(val block: net.minecraft.block.Block) : TargetState() { + data class Block(val block: net.minecraft.block.Block) : TargetState(Type.BLOCK) { + override fun toString() = "Block of ${block.name.string.capitalize()}" + 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) + + override fun isAir() = block.defaultState.isAir } - data class Stack(val itemStack: ItemStack) : TargetState() { + data class Stack(val itemStack: ItemStack) : TargetState(Type.STACK) { + private val startStack: ItemStack = itemStack.copy() + override fun toString() = "Stack of ${startStack.item.name.string.capitalize()}" + private val block = itemStack.item.block override fun matches(state: BlockState, pos: BlockPos, world: ClientWorld) = @@ -82,5 +112,7 @@ sealed class TargetState : StateMatcher { override fun getStack(world: ClientWorld, pos: BlockPos): ItemStack = itemStack + + override fun isAir() = false } } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/ContainerTask.kt b/common/src/main/kotlin/com/lambda/interaction/material/ContainerTask.kt new file mode 100644 index 000000000..c61589a24 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/ContainerTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.task.Task + +abstract class ContainerTask : Task() { + private var finish = false + private val delay = 5 + private var currentDelay = 0 + + fun delayedFinish() { + finish = true + } + + init { + listen { + if (finish) { + if (currentDelay++ > delay) success() + } + } + } +} \ 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/container/ContainerManager.kt similarity index 71% rename from common/src/main/kotlin/com/lambda/interaction/material/ContainerManager.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/ContainerManager.kt index ca383f546..714124825 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/ContainerManager.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/ContainerManager.kt @@ -15,15 +15,17 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material +package com.lambda.interaction.material.container import com.lambda.core.Loadable +import com.lambda.event.events.InventoryEvent import com.lambda.event.events.PlayerEvent -import com.lambda.event.events.ScreenHandlerEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.StackSelection import com.lambda.interaction.material.StackSelection.Companion.select -import com.lambda.interaction.material.container.* -import com.lambda.module.modules.client.TaskFlow +import com.lambda.interaction.material.container.containers.ChestContainer +import com.lambda.interaction.material.container.containers.EnderChestContainer +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.util.BlockUtils.blockEntity import com.lambda.util.BlockUtils.item import com.lambda.util.Communication.info @@ -35,14 +37,14 @@ 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.ScreenHandlerType // ToDo: Make this a Configurable to save container caches. Should use a cached region based storage system. object ContainerManager : Loadable { private val container: List - get() = compileContainers + runtimeContainers + // ToDo: Filter containers based on a filter setting TaskFlowModule.inventory.accessEnderChest etc + get() = compileContainers.filter { it !is EnderChestContainer } + runtimeContainers private val compileContainers = getInstances { forPackages("com.lambda.interaction.material.container") } @@ -57,7 +59,7 @@ object ContainerManager : Loadable { lastInteractedBlockEntity = it.blockHitResult.blockPos.blockEntity(world) } - listen { event -> + listen { event -> if (event.screenHandler !is GenericContainerScreenHandler) return@listen val handler = event.screenHandler @@ -87,39 +89,38 @@ object ContainerManager : Loadable { } } - fun container() = container.flatMap { - setOf(it) + it.shulkerContainer - }.sorted() + fun container() = container.flatMap { setOf(it) + it.shulkerContainer }.sorted() fun StackSelection.transfer(destination: MaterialContainer) = - findContainerWithSelection(this)?.transfer(this, destination) + findContainerWithMaterial(this)?.transfer(this, destination) fun findContainer( block: (MaterialContainer) -> Boolean ): MaterialContainer? = container().find(block) - fun findContainerWithSelection( + fun findContainerWithMaterial( selection: StackSelection ): MaterialContainer? = - container().find { it.available(selection) >= selection.count } + containerWithMaterial(selection).firstOrNull() - fun containerMatchSelection( + fun findContainerWithSpace( 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()) + containerWithSpace(selection).firstOrNull() + + fun containerWithMaterial( + selection: StackSelection + ): List = + container() + .sortedWith(TaskFlowModule.inventory.providerPriority.materialComparator(selection)) + .filter { it.materialAvailable(selection) >= selection.count } + + fun containerWithSpace( + selection: StackSelection + ): List = + container() + .sortedWith(TaskFlowModule.inventory.providerPriority.spaceComparator(selection)) + .filter { it.spaceAvailable(selection) >= selection.count } fun findBestAvailableTool( blockState: BlockState, @@ -129,13 +130,13 @@ object ContainerManager : Loadable { }.filter { (item, speed) -> speed > 1.0 && item.isSuitableFor(blockState) - && findContainerWithSelection(item.select()) != null + && containerWithMaterial(item.select()).isNotEmpty() }.maxByOrNull { it.second }?.first fun findDisposable() = container().find { container -> - TaskFlow.disposables.any { container.available(it.item.select()) >= 0 } + TaskFlowModule.inventory.disposables.any { container.materialAvailable(it.item.select()) >= 0 } } class NoContainerFound(selection: StackSelection) : Exception("No container found matching $selection") 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 deleted file mode 100644 index 126822684..000000000 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/CreativeContainer.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.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") -} 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 deleted file mode 100644 index b2955d691..000000000 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/MainHandContainer.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.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)) - } - } -} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt similarity index 62% rename from common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt index c825e5a07..312b19ef3 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/MaterialContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/MaterialContainer.kt @@ -15,10 +15,11 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material +package com.lambda.interaction.material.container -import com.lambda.interaction.material.StackSelection.Companion.select -import com.lambda.interaction.material.container.ShulkerBoxContainer +import com.lambda.context.SafeContext +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.containers.ShulkerBoxContainer import com.lambda.interaction.material.transfer.TransferResult import com.lambda.task.Task import com.lambda.util.Nameable @@ -27,13 +28,41 @@ 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 com.lambda.util.text.* import net.minecraft.item.ItemStack +import net.minecraft.text.Text // ToDo: Make jsonable to persistently store them abstract class MaterialContainer( - private val rank: Rank + val rank: Rank ) : Nameable, Comparable { abstract var stacks: List + abstract val description: Text + + @TextDsl + fun TextBuilder.stock(selection: StackSelection) { + literal("\n") + literal("Contains ") + val available = materialAvailable(selection) + highlighted(if (available == Int.MAX_VALUE) "∞" else available.toString()) + literal(" of ") + highlighted("${selection.optimalStack?.name?.string}") + literal("\n") + literal("Could store ") + val left = spaceAvailable(selection) + highlighted(if (left == Int.MAX_VALUE) "∞" else left.toString()) + literal(" of ") + highlighted("${selection.optimalStack?.name?.string}") + } + + fun description(selection: StackSelection) = + buildText { + text(description) + stock(selection) + } + + override val name: String + get() = buildText { text(description) }.string val shulkerContainer get() = @@ -51,46 +80,49 @@ abstract class MaterialContainer( this.stacks = stacks } + class Nothing(override val name: String = "Nothing") : Task() { + override fun SafeContext.onStart() { + success() + } + } + /** * Withdraws items from the container to the player's inventory. */ @Task.Ta5kBuilder - abstract fun withdraw(selection: StackSelection): Task<*> + open fun withdraw(selection: StackSelection): Task<*> = Nothing(name) /** * Deposits items from the player's inventory into the container. */ @Task.Ta5kBuilder - abstract fun deposit(selection: StackSelection): Task<*> + open fun deposit(selection: StackSelection): Task<*> = Nothing(name) open fun matchingStacks(selection: StackSelection) = selection.filterStacks(stacks) - open fun matchingStacks(selection: (ItemStack) -> Boolean) = - matchingStacks(selection.select()) - - open fun available(selection: StackSelection) = + open fun materialAvailable(selection: StackSelection) = matchingStacks(selection).count - open fun spaceLeft(selection: StackSelection) = + open fun spaceAvailable(selection: StackSelection) = matchingStacks(selection).spaceLeft + stacks.empty * selection.stackSize fun transfer(selection: StackSelection, destination: MaterialContainer): TransferResult { - val amount = available(selection) + val amount = materialAvailable(selection) if (amount < selection.count) { return TransferResult.MissingItems(selection.count - amount) } -// val space = destination.spaceLeft(selection) +// val space = destination.spaceAvailable(selection) // if (space == 0) { // return TransferResult.NoSpace // } -// + // val transferAmount = minOf(amount, space) // selection.selector = { true } // selection.count = transferAmount - return TransferResult.Transfer(selection, from = this, to = destination) + return TransferResult.ContainerTransfer(selection, from = this, to = destination) } enum class Rank { 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 deleted file mode 100644 index e2a86fae7..000000000 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/OffHandContainer.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.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, - ), - ) - } - } - } -} 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 deleted file mode 100644 index 63565b0b6..000000000 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/ShulkerBoxContainer.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.interaction.material.container - -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.BuildTask.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 - -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(this@Withdraw) { _, placePos -> - openContainer(placePos).thenRun(this@Withdraw) { _, screen -> - withdraw(screen, selection).thenRun(this@Withdraw) { _, _ -> - 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(this@Deposit) { _, placePos -> - openContainer(placePos).thenRun(this@Deposit) { _, screen -> - deposit(screen, selection).thenRun(this@Deposit) { _, _ -> - breakAndCollectBlock(placePos).onSuccess { _, _ -> - success(Unit) - } - } - } - }.start(this@Deposit) - } - } - - override fun deposit(selection: StackSelection) = Deposit(selection, shulkerStack) -} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt similarity index 63% rename from common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt index 92a8a469b..8bb41286e 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/ChestContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt @@ -15,16 +15,18 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.container +package com.lambda.interaction.material.container.containers -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.interaction.material.transfer.SlotTransfer.Companion.deposit +import com.lambda.interaction.material.transfer.SlotTransfer.Companion.withdraw +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.task.tasks.OpenContainer import com.lambda.util.Communication.info +import com.lambda.util.text.buildText +import com.lambda.util.text.highlighted +import com.lambda.util.text.literal import net.minecraft.item.ItemStack -import net.minecraft.screen.GenericContainerScreenHandler import net.minecraft.screen.ScreenHandler import net.minecraft.util.math.BlockPos @@ -33,7 +35,16 @@ data class ChestContainer( val blockPos: BlockPos, val containedInStash: StashContainer? = null, ) : MaterialContainer(Rank.CHEST) { - override val name = "Chest at ${blockPos.toShortString()}" + override val description = + buildText { + literal("Chest at ") + highlighted(blockPos.toShortString()) + containedInStash?.let { stash -> + literal(" (contained in ") + highlighted(stash.name) + literal(")") + } + } // override fun prepare() = // moveIntoEntityRange(blockPos).onSuccess { _, _ -> @@ -47,21 +58,17 @@ data class ChestContainer( // } 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) + OpenContainer(blockPos) + .then { + info("Withdrawing $selection from ${it.type}") + withdraw(it, selection) } 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) + OpenContainer(blockPos) + .then { + info("Depositing $selection to ${it.type}") + deposit(it, selection) } class ChestBlockedException : Exception("The chest is blocked by another block or a cat") diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt new file mode 100644 index 000000000..52683117a --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/CreativeContainer.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.container.containers + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.task.Task +import com.lambda.util.item.ItemStackUtils.equal +import com.lambda.util.text.buildText +import com.lambda.util.text.literal +import net.minecraft.item.ItemStack + +data object CreativeContainer : MaterialContainer(Rank.CREATIVE) { + override var stacks = emptyList() + + override val description = buildText { literal("Creative") } + + override fun materialAvailable(selection: StackSelection): Int = + if (mc.player?.isCreative == true && selection.optimalStack != null) Int.MAX_VALUE else 0 + + override fun spaceAvailable(selection: StackSelection): Int = + if (mc.player?.isCreative == true && selection.optimalStack != null) Int.MAX_VALUE else 0 + + class CreativeDeposit @Ta5kBuilder constructor(val selection: StackSelection) : Task() { + override val name: String get() = "Removing $selection from creative inventory" + + override fun SafeContext.onStart() { + if (!player.isCreative) { + // ToDo: Maybe switch gamemode? + throw NotInCreativeModeException() + } + + player.currentScreenHandler?.slots?.let { slots -> + selection.filterSlots(slots).forEach { + interaction.clickCreativeStack(ItemStack.EMPTY, it.id) + } + } + + success() + } + } + + override fun deposit(selection: StackSelection) = CreativeDeposit(selection) + + class CreativeWithdrawal @Ta5kBuilder constructor(val selection: StackSelection) : Task() { + override val name: String get() = "Withdrawing $selection from creative inventory" + + override fun SafeContext.onStart() { + selection.optimalStack?.let { optimalStack -> + if (player.mainHandStack.equal(optimalStack)) return + + if (!player.isCreative) { + // ToDo: Maybe switch gamemode? + throw NotInCreativeModeException() + } + + interaction.clickCreativeStack( + optimalStack, + 36 + player.inventory.selectedSlot + ) + success() + return + } + + throw NoOptimalStackException() + } + } + + // Withdraws items from the creative menu to the player's main hand + override fun withdraw(selection: StackSelection) = CreativeWithdrawal(selection) + + class NotInCreativeModeException : IllegalStateException("Insufficient permission: not in creative mode") + class NoOptimalStackException : IllegalStateException("Cannot move item: no optimal stack") +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt similarity index 59% rename from common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt index 1500596a8..398549eac 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/EnderChestContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/EnderChestContainer.kt @@ -15,24 +15,24 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.container +package com.lambda.interaction.material.container.containers -import com.lambda.interaction.material.MaterialContainer +import com.lambda.context.SafeContext import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.MaterialContainer import com.lambda.task.Task -import com.lambda.task.Task.Companion.emptyTask -import com.lambda.task.Task.Companion.failTask -import com.lambda.task.tasks.OpenContainer.Companion.openContainer import com.lambda.util.Communication.info +import com.lambda.util.text.buildText +import com.lambda.util.text.literal 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 val description = buildText { literal("Ender Chest") } + // override fun prepare(): Task<*> { // TODO("Not yet implemented") // } @@ -47,13 +47,25 @@ object EnderChestContainer : MaterialContainer(Rank.ENDER_CHEST) { // } // } - override fun withdraw(selection: StackSelection): Task<*> { - info("Not yet implemented") - return emptyTask() + class EnderchestWithdrawal @Ta5kBuilder constructor(selection: StackSelection) : Task() { + override val name = "Withdrawing $selection from ender chest" + + override fun SafeContext.onStart() { + info("Not yet implemented") + success() + } } - override fun deposit(selection: StackSelection): Task<*> { - info("Not yet implemented") - return emptyTask() + override fun withdraw(selection: StackSelection) = EnderchestWithdrawal(selection) + + class EnderchestDeposit @Ta5kBuilder constructor(selection: StackSelection) : Task() { + override val name = "Depositing $selection into ender chest" + + override fun SafeContext.onStart() { + info("Not yet implemented") + success() + } } + + override fun deposit(selection: StackSelection) = EnderchestDeposit(selection) } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt new file mode 100644 index 000000000..a9000639e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/HotbarContainer.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.container.containers + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.interaction.material.ContainerTask +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.transfer.SlotTransfer.Companion.deposit +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.util.player.SlotUtils.hotbar +import com.lambda.util.text.buildText +import com.lambda.util.text.literal +import net.minecraft.item.ItemStack + +object HotbarContainer : MaterialContainer(Rank.HOTBAR) { + override var stacks: List + get() = mc.player?.hotbar ?: emptyList() + set(_) {} + + override val description = buildText { literal("Hotbar") } + + class HotbarDeposit @Ta5kBuilder constructor(val selection: StackSelection) : ContainerTask() { + override val name: String get() = "Depositing $selection into hotbar" + + override fun SafeContext.onStart() { + val handler = player.currentScreenHandler + deposit(handler, selection).finally { + delayedFinish() + }.execute(this@HotbarDeposit) + } + } + + override fun deposit(selection: StackSelection) = HotbarDeposit(selection) +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt similarity index 68% rename from common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt index 2f8ca8c4e..928bbd113 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/InventoryContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt @@ -15,23 +15,19 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.container +package com.lambda.interaction.material.container.containers 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.interaction.material.container.MaterialContainer import com.lambda.util.player.SlotUtils.combined +import com.lambda.util.text.buildText +import com.lambda.util.text.literal 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") + override val description = buildText { literal("Inventory") } } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt new file mode 100644 index 000000000..5e26d6644 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/MainHandContainer.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.container.containers + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.interaction.material.ContainerTask +import com.lambda.interaction.material.transfer.TransactionExecutor.Companion.transfer +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.util.item.ItemStackUtils.equal +import com.lambda.util.player.SlotUtils.combined +import com.lambda.util.player.SlotUtils.hotbar +import com.lambda.util.player.SlotUtils.storage +import com.lambda.util.text.buildText +import com.lambda.util.text.literal +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand + +object MainHandContainer : MaterialContainer(Rank.MAIN_HAND) { + override var stacks: List + get() = mc.player?.mainHandStack?.let { listOf(it) } ?: emptyList() + set(_) {} + + override val description = buildText { literal("MainHand") } + + class HandDeposit @Ta5kBuilder constructor(val selection: StackSelection, val hand: Hand) : ContainerTask() { + override val name: String get() = "Depositing [$selection] to ${hand.name.lowercase().replace("_", " ")}" + + override fun SafeContext.onStart() { + val moveStack = InventoryContainer.matchingStacks(selection).firstOrNull() ?: run { + failure("No matching stacks found in inventory") + return + } + + val handStack = player.getStackInHand(hand) + if (moveStack.equal(handStack)) { + success() + return + } + + transfer { + val stackInOffHand = moveStack.equal(player.offHandStack) + if (hand == Hand.MAIN_HAND && stackInOffHand) { + swapHands() + return@transfer + } + + when (moveStack) { + in player.hotbar -> swapToHotbarSlot(player.hotbar.indexOf(moveStack)) + in player.storage -> pickFromInventory(player.combined.indexOf(moveStack)) + } + + if (hand == Hand.OFF_HAND) swapHands() + }.finally { + success() + }.execute(this@HandDeposit) + } + } + + override fun deposit(selection: StackSelection) = HandDeposit(selection, Hand.MAIN_HAND) +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt similarity index 54% rename from common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt index 55b75ae47..d19a7a0a6 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/HotbarContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/OffHandContainer.kt @@ -15,27 +15,22 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.container +package com.lambda.interaction.material.container.containers 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 com.lambda.interaction.material.container.MaterialContainer +import com.lambda.util.text.buildText +import com.lambda.util.text.literal import net.minecraft.item.ItemStack +import net.minecraft.util.Hand -object HotbarContainer : MaterialContainer(Rank.HOTBAR) { +object OffHandContainer : MaterialContainer(Rank.OFF_HAND) { override var stacks: List - get() = mc.player?.hotbar ?: emptyList() + get() = mc.player?.offHandStack?.let { listOf(it) } ?: emptyList() set(_) {} - override val name = "Hotbar" - override fun withdraw(selection: StackSelection) = emptyTask("WithdrawFromHotbar") + override val description = buildText { literal("OffHand") } - override fun deposit(selection: StackSelection): Task<*> { - val handler = mc.player?.currentScreenHandler ?: return emptyTask("NoScreenHandler") - return deposit(handler, selection) - } + override fun deposit(selection: StackSelection) = MainHandContainer.HandDeposit(selection, Hand.OFF_HAND) } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt new file mode 100644 index 000000000..52cc0da78 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/ShulkerBoxContainer.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.container.containers + +import com.lambda.context.SafeContext +import com.lambda.interaction.material.StackSelection +import com.lambda.task.Task +import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock +import com.lambda.interaction.material.transfer.SlotTransfer.Companion.deposit +import com.lambda.interaction.material.transfer.SlotTransfer.Companion.withdraw +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.task.tasks.OpenContainer +import com.lambda.task.tasks.PlaceContainer +import com.lambda.util.text.buildText +import com.lambda.util.text.highlighted +import com.lambda.util.text.literal +import net.minecraft.item.ItemStack + +data class ShulkerBoxContainer( + override var stacks: List, + val containedIn: MaterialContainer, + val shulkerStack: ItemStack, +) : MaterialContainer(Rank.SHULKER_BOX) { + override val description = + buildText { + highlighted(shulkerStack.name.string) + literal(" in ") + highlighted(containedIn.name) + literal(" in slot ") + highlighted("$slotInContainer") + } + + private val slotInContainer: Int get() = containedIn.stacks.indexOf(shulkerStack) + + class ShulkerWithdraw( + private val selection: StackSelection, + private val shulkerStack: ItemStack + ) : Task() { + override val name = "Withdraw $selection from ${shulkerStack.name.string}" + + override fun SafeContext.onStart() { + PlaceContainer(shulkerStack).then { placePos -> + OpenContainer(placePos).then { screen -> + withdraw(screen, selection).then { + breakAndCollectBlock(placePos).finally { + success() + } + } + } + }.execute(this@ShulkerWithdraw) + } + } + + override fun withdraw(selection: StackSelection) = ShulkerWithdraw(selection, shulkerStack) + + class ShulkerDeposit( + private val selection: StackSelection, + private val shulkerStack: ItemStack + ) : Task() { + override val name = "Deposit $selection into ${shulkerStack.name.string}" + + override fun SafeContext.onStart() { + PlaceContainer(shulkerStack).then { placePos -> + OpenContainer(placePos).then { screen -> + deposit(screen, selection).then { + breakAndCollectBlock(placePos).finally { + success() + } + } + } + }.execute(this@ShulkerDeposit) + } + } + + override fun deposit(selection: StackSelection) = ShulkerDeposit(selection, shulkerStack) +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt similarity index 68% rename from common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt rename to common/src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt index fd078fae2..647f2e7a5 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/container/StashContainer.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/container/containers/StashContainer.kt @@ -15,12 +15,14 @@ * along with this program. If not, see . */ -package com.lambda.interaction.material.container +package com.lambda.interaction.material.container.containers -import com.lambda.interaction.material.MaterialContainer import com.lambda.interaction.material.StackSelection -import com.lambda.task.Task +import com.lambda.interaction.material.container.MaterialContainer import com.lambda.util.math.VecUtils.blockPos +import com.lambda.util.text.buildText +import com.lambda.util.text.highlighted +import com.lambda.util.text.literal import net.minecraft.item.ItemStack import net.minecraft.util.math.Box @@ -31,18 +33,14 @@ data class StashContainer( 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 val description = buildText { + literal("Stash at ") + highlighted(pos.center.blockPos.toShortString()) } - override fun deposit(selection: StackSelection): Task<*> { - TODO("Not yet implemented") - } - - override fun available(selection: StackSelection): Int = + override fun materialAvailable(selection: StackSelection): Int = chests.sumOf { - it.available(selection) + it.materialAvailable(selection) } } diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryChanges.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryChanges.kt new file mode 100644 index 000000000..cffaed390 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryChanges.kt @@ -0,0 +1,82 @@ +package com.lambda.interaction.material.transfer + +import com.lambda.interaction.material.StackSelection +import com.lambda.util.item.ItemStackUtils.equal +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot + +/** + * A class representing changes in an inventory's state over time. It acts as a tracker for + * detecting, storing, and merging differences between the original and updated states of + * inventory slots. This class extends a map-like structure, where the key is the slot index + * and the value is a list of pairs representing the original and updated states of an inventory slot. + * + * Example: + * ``` + * #0: 64 obsidian -> 0 air, 0 air -> 64 obsidian + * #36: 12 observer -> 11 observer + * ``` + * - Where `#0` is the slot id, first it was emptied and then got `64 obsidian` again + * - Where `#36` is the slot id, that was reduced by `1 observer` + * + * @property slots A list of `Slot` objects representing the current inventory slots being tracked. + * Defaults to an empty list. + */ +class InventoryChanges( + private var slots: List, +) : MutableMap>> by HashMap() { + private val originalStacks = slots.map { it.stack.copy() } + + /** + * Detect and store changes directly in the map. + */ + fun detectChanges() { + require(slots.isNotEmpty()) { "Cannot detect changes on an empty slots list" } + slots.forEachIndexed { index, slot -> + val originalStack = originalStacks[index] + val updatedStack = slot.stack + if (!originalStack.equal(updatedStack)) { + getOrPut(index) { mutableListOf() }.add(originalStack to updatedStack.copy()) + } + } + } + + /** + * Create a new `InventoryChanges` object that merges this changes object with another one. + * + * @param other Another `InventoryChanges` instance to merge with. + * @return A new `InventoryChanges` instance containing merged entries. + */ + infix fun merge(other: InventoryChanges) { + require(slots.isNotEmpty()) { "Cannot merge changes to an empty slots list" } + other.forEach { (key, value) -> + getOrPut(key) { mutableListOf() }.addAll(value) + } + } + + /** + * Evaluates if the current inventory changes fulfill the given selection requirement. + * + * @param to A list of `Slot` objects to evaluate for the selection criteria. + * @param selection A `StackSelection` object that specifies the selection criteria, including the + * target items and their required count. + * @return `true` if the total count of matching items across the filtered slots meets or exceeds + * the required count specified in the `StackSelection`; `false` otherwise. + */ + fun fulfillsSelection(to: List, selection: StackSelection): Boolean { + require(slots.isNotEmpty()) { "Cannot evaluate selection on an empty slots list" } + val targetSlots = selection.filterSlots(to).map { it.id } + return filter { it.key in targetSlots }.entries.sumOf { (_, changes) -> + changes.lastOrNull()?.second?.count ?: 0 + } >= selection.count + } + + override fun toString() = + if (entries.isEmpty()) { + "No changes detected" + } else { + entries.joinToString("\n") { key -> + "#${key.key}: ${key.value.joinToString { "${it.first} -> ${it.second}" }}" + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryTransaction.kt new file mode 100644 index 000000000..b656214bd --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/InventoryTransaction.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer + +import com.lambda.context.SafeContext +import com.lambda.task.Task +import com.lambda.threading.runSafe + +abstract class InventoryTransaction : Task() { + private lateinit var changes: InventoryChanges + + override fun SafeContext.onStart() { + changes = InventoryChanges(player.currentScreenHandler.slots) + } + + fun finish() { + runSafe { + changes.detectChanges() + success(changes) + } ?: failure("Failed to finish transaction") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/SlotTransfer.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/SlotTransfer.kt new file mode 100644 index 000000000..f19cd1c8f --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/SlotTransfer.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer + +import com.lambda.Lambda.LOG +import com.lambda.config.groups.InventoryConfig +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.transfer.TransactionExecutor.Companion.transfer +import com.lambda.module.modules.client.TaskFlowModule +import com.lambda.task.Task +import com.lambda.util.extension.containerSlots +import com.lambda.util.extension.inventorySlots +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot + +class SlotTransfer @Ta5kBuilder constructor( + val screen: ScreenHandler, + private val selection: StackSelection, + val from: List, + val to: List, + private val closeScreen: Boolean = true, + private val settings: InventoryConfig = TaskFlowModule.inventory +) : Task() { + private var selectedFrom = selection.filterSlots(from) + private var selectedTo = to.filter { it.stack.isEmpty } // + to.filter { it.stack.item.block in TaskFlowModule.disposables } + override val name: String + get() = "Moving $selection from slots [${selectedFrom.joinToString { "${it.id}" }}] to slots [${selectedTo.joinToString { "${it.id}" }}] in ${screen::class.simpleName}" + + private var delay = 0 + private lateinit var changes: InventoryChanges + + override fun SafeContext.onStart() { + changes = InventoryChanges(player.currentScreenHandler.slots) + } + + init { + listen { + val current = player.currentScreenHandler + if (current != screen) { + failure("Screen has changed. Expected ${screen::class.simpleName} (revision ${screen.revision}, got ${current::class.simpleName} (revision ${current.revision})") + return@listen + } + + if (changes.fulfillsSelection(to, selection)) { + if (closeScreen) player.closeHandledScreen() + success() + return@listen + } + + if (--delay >= 0) return@listen + delay = settings.actionTimout + + selectedFrom = selection.filterSlots(from) + selectedTo = to.filter { it.stack.isEmpty } // + to.filter { it.stack.item.block in TaskFlowModule.disposables } + + val nextFrom = selectedFrom.firstOrNull() ?: return@listen + val nextTo = selectedTo.firstOrNull() ?: return@listen + + LOG.info("Changes so far:\n$changes") + + transfer { + moveSlot(nextFrom.id, nextTo.id) + }.finally { change -> + changes merge change + }.execute(this@SlotTransfer) + +// if (transfer.fulfillsSelection(selection)) { +// info("Transfer complete") +//// success() +//// if (closeScreen) player.closeHandledScreen() +// return@listen +// } + } + } + + companion object { + @Ta5kBuilder + fun moveItems( + screen: ScreenHandler, + selection: StackSelection, + from: List, + to: List, + closeScreen: Boolean = true + ) = SlotTransfer(screen, selection, from, to, closeScreen) + + @Ta5kBuilder + fun withdraw(screen: ScreenHandler, selection: StackSelection, closeScreen: Boolean = true) = + moveItems(screen, selection, screen.containerSlots, screen.inventorySlots, closeScreen) + + @Ta5kBuilder + fun deposit(screen: ScreenHandler, selection: StackSelection, closeScreen: Boolean = true) = + moveItems(screen, selection, screen.inventorySlots, screen.containerSlots, closeScreen) + } +} diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransactionExecutor.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransactionExecutor.kt new file mode 100644 index 000000000..048cb2291 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransactionExecutor.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer + +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.transaction.* +import com.lambda.task.Task +import net.minecraft.screen.slot.SlotActionType + +class TransactionExecutor @Ta5kBuilder constructor( + private val transactions: MutableList = mutableListOf() +) : Task() { + override val name: String get() = "Execution of ${transactions.size} transactions left" + + private lateinit var changes: InventoryChanges + + override fun SafeContext.onStart() { + changes = InventoryChanges(player.currentScreenHandler.slots) + } + + init { + listen { + if (transactions.isEmpty()) { + success(changes) + return@listen + } + + transactions.removeFirstOrNull()?.finally { change -> + changes merge change + }?.execute(this@TransactionExecutor) + } + } + + @DslMarker + annotation class InvTransfer + + @InvTransfer + fun click(slotId: Int, button: Int, actionType: SlotActionType) { + transactions.add(ClickSlotTransaction(slotId, button, actionType)) + } + + @InvTransfer + fun pickFromInventory(slotId: Int) { + transactions.add(PickFromInventoryTransaction(slotId)) + } + + @InvTransfer + fun dropItemInHand(entireStack: Boolean = true) { + transactions.add(DropItemInHandTransaction(entireStack)) + } + + @InvTransfer + fun swapHands() { + transactions.add(SwapHandsTransaction()) + } + + @InvTransfer + fun swapToHotbarSlot(slotId: Int) { + transactions.add(SwapHotbarSlotTransaction(slotId)) + } + + @InvTransfer + fun pickup(slotId: Int, button: Int = 0) = click(slotId, button, SlotActionType.PICKUP) + + // Quick move action (Shift-click) + @InvTransfer + fun quickMove(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_MOVE) + + @InvTransfer + fun swap(slotId: Int, hotbarSlot: Int) = click(slotId, hotbarSlot, SlotActionType.SWAP) + + // Clone action (Creative mode) + @InvTransfer + fun clone(slotId: Int) = click(slotId, 2, SlotActionType.CLONE) + + // Throw stack or single item + @InvTransfer + fun throwStack(slotId: Int) = click(slotId, 1, SlotActionType.THROW) + @InvTransfer + fun throwSingle(slotId: Int) = click(slotId, 0, SlotActionType.THROW) + + // Quick craft action + @InvTransfer + fun quickCraftStart(slotId: Int) = click(slotId, 0, SlotActionType.QUICK_CRAFT) + @InvTransfer + fun quickCraftDrag(slotId: Int) = click(slotId, 1, SlotActionType.QUICK_CRAFT) + @InvTransfer + fun quickCraftEnd(slotId: Int) = click(slotId, 2, SlotActionType.QUICK_CRAFT) + + // Pickup all items (double-click) + @InvTransfer + fun pickupAll(slotId: Int) = click(slotId, 0, SlotActionType.PICKUP_ALL) + + // Helper function: Move items from one slot to another + @InvTransfer + fun moveSlot(fromSlotId: Int, toSlotId: Int, button: Int = 0) { + pickup(fromSlotId, button) + pickup(toSlotId, button) + } + + // Helper function: Split a stack into two + @InvTransfer + fun splitStack(slotId: Int, targetSlotId: Int) { + pickup(slotId, 1) // Pickup half the stack + pickup(targetSlotId, 0) // Place it in the target slot + } + + // Helper function: Merge stacks + @InvTransfer + fun mergeStacks(sourceSlotId: Int, targetSlotId: Int) { + pickup(sourceSlotId, 0) + pickup(targetSlotId, 0) + } + + companion object { + @InvTransfer + fun transfer(block: TransactionExecutor.() -> Unit) = + TransactionExecutor().apply { + block(this) + } + } +} \ 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 index 7e0f1cea4..a64f961e9 100644 --- a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferResult.kt +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferResult.kt @@ -18,28 +18,30 @@ package com.lambda.interaction.material.transfer import com.lambda.context.SafeContext -import com.lambda.interaction.material.MaterialContainer +import com.lambda.interaction.material.container.MaterialContainer import com.lambda.interaction.material.StackSelection import com.lambda.task.Task abstract class TransferResult : Task() { - data class Transfer( + data class ContainerTransfer( val selection: StackSelection, val from: MaterialContainer, val to: MaterialContainer ) : TransferResult() { + override val name = "Container Transfer of [$selection] from [${from.name}] to [${to.name}]" + override fun SafeContext.onStart() { - from.withdraw(selection).thenRun(this@Transfer) { _, _ -> - to.deposit(selection).onSuccess { _, _ -> - success(Unit) + from.withdraw(selection).then { + to.deposit(selection).finally { + success() } - }.start(this@Transfer) + }.execute(this@ContainerTransfer) } - - override fun toString() = "Transfer of [$selection] from [${from.name}] to [${to.name}]" } data object NoSpace : TransferResult() { + override val name = "No space left in the target container" + // ToDo: Needs inventory space resolver. compressing or disposing override fun SafeContext.onStart() { failure("No space left in the target container") @@ -47,6 +49,8 @@ abstract class TransferResult : Task() { } data class MissingItems(val missing: Int) : TransferResult() { + override val name = "Missing $missing items" + // ToDo: Find other satisfying permutations override fun SafeContext.onStart() { failure("Missing $missing items") diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferSelection.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferSelection.kt new file mode 100644 index 000000000..ac8ed4c62 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/TransferSelection.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer + +class TransferSelection { +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/ClickSlotTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/ClickSlotTransaction.kt new file mode 100644 index 000000000..5c4ff8af8 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/ClickSlotTransaction.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.InventoryTransaction +import com.lambda.util.player.SlotUtils.clickSlot +import net.minecraft.screen.slot.SlotActionType + +class ClickSlotTransaction @Ta5kBuilder constructor( + private val slotId: Int, + private val button: Int, + private val actionType: SlotActionType +) : InventoryTransaction() { + override val name: String get() = "Click slot #$slotId with action $actionType and button $button" + + init { + listen { + clickSlot(slotId, button, actionType) + } + + listen { + finish() + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/DropItemInHandTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/DropItemInHandTransaction.kt new file mode 100644 index 000000000..e480c6748 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/DropItemInHandTransaction.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.InventoryTransaction +import net.minecraft.util.Hand + +class DropItemInHandTransaction @Ta5kBuilder constructor( + private val entireStack: Boolean = false +) : InventoryTransaction() { + override val name: String get() = "Dropping ${if (entireStack) "stack" else "item"} in hand" + + init { + listen { + if (!player.isSpectator && player.dropSelectedItem(entireStack)) { + player.swingHand(Hand.MAIN_HAND) + } + } + + listen { + finish() + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/PickFromInventoryTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/PickFromInventoryTransaction.kt new file mode 100644 index 000000000..9197741fe --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/PickFromInventoryTransaction.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.event.events.InventoryEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.InventoryTransaction + +class PickFromInventoryTransaction @Ta5kBuilder constructor( + val slot: Int, +) : InventoryTransaction() { + override val name: String get() = "Picking from slot #$slot" + private var confirming = false + + init { + listen { + if (confirming) return@listen + + interaction.pickFromInventory(slot) + confirming = true + } + + listen { + finish() + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/QuickCraftTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/QuickCraftTransaction.kt new file mode 100644 index 000000000..007f2d1d3 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/QuickCraftTransaction.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.interaction.material.transfer.InventoryTransaction + +class QuickCraftTransaction @Ta5kBuilder constructor( + private val slots: List, + private val mode: Mode = Mode.SINGLE +) : InventoryTransaction() { + override val name: String get() = "Drag and drop ${slots.size} slots" + + enum class Mode { + SINGLE, SPLIT + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHandsTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHandsTransaction.kt new file mode 100644 index 000000000..5c20afb80 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHandsTransaction.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.event.events.InventoryEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.InventoryTransaction +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction + +class SwapHandsTransaction @Ta5kBuilder constructor() : InventoryTransaction() { + override val name: String get() = "Swap Hand Stacks" + + init { + listen { + if (player.isSpectator) return@listen + connection.sendPacket( + PlayerActionC2SPacket( + PlayerActionC2SPacket.Action.SWAP_ITEM_WITH_OFFHAND, + BlockPos.ORIGIN, + Direction.DOWN + ) + ) + } + + listen { + finish() + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHotbarSlotTransaction.kt b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHotbarSlotTransaction.kt new file mode 100644 index 000000000..5b302af17 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/interaction/material/transfer/transaction/SwapHotbarSlotTransaction.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.material.transfer.transaction + +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.transfer.InventoryTransaction + +class SwapHotbarSlotTransaction @Ta5kBuilder constructor( + val slot: Int +) : InventoryTransaction() { + override val name: String get() = "Selecting slot #$slot" + + init { + listen { + player.inventory.selectedSlot = slot + } + + listen { + finish() + } + } +} \ 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 520001059..39ed9948b 100644 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt +++ b/common/src/main/kotlin/com/lambda/interaction/rotation/Rotation.kt @@ -72,19 +72,25 @@ data class Rotation(val yaw: Double, val pitch: Double) { box.raycast(eyeVec, eyeVec + vector * reach).orElse(null) } - val Direction.yaw: Float - get() = when (this) { - Direction.NORTH -> -180.0f - Direction.SOUTH -> 0.0f - Direction.EAST -> -90.0f - Direction.WEST -> 90.0f - else -> 0.0f - } - companion object { + val Direction.yaw: Float + get() = when (this) { + Direction.NORTH -> -180.0f + Direction.SOUTH -> 0.0f + Direction.EAST -> -90.0f + Direction.WEST -> 90.0f + else -> 0.0f + } + val ZERO = Rotation(0.0, 0.0) val DOWN = Rotation(0.0, 90.0) - val Entity.rotation get() = Rotation(yaw, pitch) + val UP = Rotation(0.0, -90.0) + val Direction.rotation get() = Rotation(yaw.toDouble(), 0.0) + var Entity.rotation get() = Rotation(yaw, pitch) + set(value) { + yaw = value.yawF + pitch = value.pitchF + } fun wrap(deg: Double) = MathHelper.wrapDegrees(deg) 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 409491cfd..84dd5fbfd 100644 --- a/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt +++ b/common/src/main/kotlin/com/lambda/interaction/rotation/RotationContext.kt @@ -17,13 +17,13 @@ package com.lambda.interaction.rotation -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.util.world.raycast.RayCastUtils.orMiss import net.minecraft.util.hit.HitResult data class RotationContext( val rotation: Rotation, - val config: IRotationConfig, + val config: RotationConfig, val hitResult: HitResult? = null, val verify: HitResult.() -> Boolean = { true }, ) { 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 4d1d2d980..9d1b14339 100644 --- a/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt +++ b/common/src/main/kotlin/com/lambda/interaction/visibilty/VisibilityChecker.kt @@ -17,17 +17,18 @@ package com.lambda.interaction.visibilty -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.config.groups.InteractionConfig import com.lambda.context.SafeContext import com.lambda.interaction.RotationManager -import com.lambda.interaction.rotation.Rotation.Companion.dist +import com.lambda.interaction.construction.verify.ScanMode +import com.lambda.interaction.construction.verify.SurfaceScan import com.lambda.interaction.rotation.Rotation.Companion.rotationTo import com.lambda.interaction.rotation.RotationContext -import com.lambda.module.modules.client.TaskFlow +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.util.BlockUtils.blockState -import com.lambda.util.math.VecUtils.distSq import com.lambda.util.extension.component6 +import com.lambda.util.math.VecUtils.distSq import com.lambda.util.world.raycast.RayCastUtils.blockResult import com.lambda.util.world.raycast.RayCastUtils.entityResult import net.minecraft.entity.Entity @@ -54,7 +55,7 @@ object VisibilityChecker { * @return A [RotationContext] if a valid rotation was found; otherwise, null. */ fun SafeContext.lookAtEntity( - rotationConfig: IRotationConfig, + rotationConfig: RotationConfig, interactionConfig: InteractionConfig, entity: Entity ) = findRotation(listOf(entity.boundingBox), rotationConfig, interactionConfig) { @@ -73,9 +74,9 @@ object VisibilityChecker { */ fun SafeContext.lookAtBlock( blockPos: BlockPos, - rotationConfig: IRotationConfig = TaskFlow.rotation, - interactionConfig: InteractionConfig = TaskFlow.interact, - sides: Set = emptySet() + rotationConfig: RotationConfig = TaskFlowModule.rotation, + interactionConfig: InteractionConfig = TaskFlowModule.interact, + sides: Set = Direction.entries.toSet() ): RotationContext? { val state = blockPos.blockState(world) val voxelShape = state.getOutlineShape(world, blockPos) @@ -99,9 +100,9 @@ object VisibilityChecker { */ fun SafeContext.findRotation( boxes: List, - rotationConfig: IRotationConfig, + rotationConfig: RotationConfig, interact: InteractionConfig, - sides: Set = emptySet(), + sides: Set = Direction.entries.toSet(), reach: Double = interact.reach, eye: Vec3d = player.getCameraPosVec(1f), verify: HitResult.() -> Boolean, @@ -117,13 +118,15 @@ object VisibilityChecker { val reachSq = reach.pow(2) boxes.forEach { box -> - scanVisibleSurfaces(eye, box, sides, interact.resolution) { _, vec -> - if (eye distSq vec > reachSq) return@scanVisibleSurfaces + val visible = visibleSides(box, eye, interact) + + scanSurfaces(box, visible.intersect(sides), interact.resolution) { _, vec -> + if (eye distSq vec > reachSq) return@scanSurfaces val newRotation = eye.rotationTo(vec) - val cast = newRotation.rayCast(reach, eye) ?: return@scanVisibleSurfaces - if (!cast.verify()) return@scanVisibleSurfaces + val cast = newRotation.rayCast(reach, eye) ?: return@scanSurfaces + if (!cast.verify()) return@scanSurfaces validHits[vec] = cast } @@ -145,48 +148,92 @@ object VisibilityChecker { } /** - * Scans the visible surfaces of a given box, identifying points on each surface within a defined resolution. - * The surface is subdivided to hits the corners of the pixels + * Scans the surfaces of a given box, optionally excluding specific sides, + * and executes a callback for each point calculated based on the scanning parameters. * - * @param eyes The player's eye position. - * @param box The bounding box of the target. - * @param sides Set of block sides to consider for visibility. - * @param resolution The number of points to sample along each axis of the box. - * @param check A lambda to check each visible point for a hit. + * @param box The 3D box whose surfaces will be scanned. + * @param excludedSides A set of directions representing the sides of the box to exclude from the scan (default is an empty set). + * @param resolution The number of intervals into which each dimension is divided for scanning (default is 5). + * @param scan Configuration specifying the axis and mode of the scan (default is `SurfaceScan.DEFAULT`). + * @param check A callback function that performs an action for each surface point, receiving the direction of the surface and the current 3D vector. */ - inline fun scanVisibleSurfaces( - eyes: Vec3d, + inline fun scanSurfaces( box: Box, - sides: Set = emptySet(), - resolution: Int = 30, + excludedSides: Set = emptySet(), + resolution: Int = 5, + scan: SurfaceScan = SurfaceScan.DEFAULT, check: (Direction, Vec3d) -> Unit, ) { - box.getVisibleSurfaces(eyes) - .forEach { side -> - if (sides.isNotEmpty() && side !in sides) return@forEach - val (minX, minY, minZ, maxX, maxY, maxZ) = box.shrink(0.01, 0.01, 0.01).bounds(side) - val stepX = (maxX - minX) / resolution - val stepY = (maxY - minY) / resolution - val stepZ = (maxZ - minZ) / resolution - (0..resolution).forEach { i -> - val x = if (stepX != 0.0) minX + stepX * i else minX - (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(side, Vec3d(x, y, z)) - } + excludedSides.forEach { side -> + if (excludedSides.isNotEmpty() && side !in excludedSides) return@forEach + val (minX, minY, minZ, maxX, maxY, maxZ) = box.shrink(0.01, 0.01, 0.01).bounds(side) + val stepX = (maxX - minX) / resolution + val stepY = (maxY - minY) / resolution + val stepZ = (maxZ - minZ) / resolution + + // Determine the bounds to scan based on the axis and mode + val (startX, endX) = if (scan.axis == Direction.Axis.X && stepX != 0.0) { + val centerX = (minX + maxX) / 2 + when (scan.mode) { + ScanMode.GREATER_HALF -> centerX + 0.01 to maxX + ScanMode.LESSER_HALF -> minX to centerX - 0.01 + ScanMode.FULL -> minX to maxX + } + } else minX to maxX + + val (startY, endY) = if (scan.axis == Direction.Axis.Y && stepY != 0.0) { + val centerY = (minY + maxY) / 2 + when (scan.mode) { + ScanMode.GREATER_HALF -> centerY + 0.01 to maxY + ScanMode.LESSER_HALF -> minY to centerY - 0.01 + ScanMode.FULL -> minY to maxY + } + } else minY to maxY + + val (startZ, endZ) = if (scan.axis == Direction.Axis.Z && stepZ != 0.0) { + val centerZ = (minZ + maxZ) / 2 + when (scan.mode) { + ScanMode.GREATER_HALF -> centerZ + 0.01 to maxZ + ScanMode.LESSER_HALF -> minZ to centerZ - 0.01 + ScanMode.FULL -> minZ to maxZ + } + } else minZ to maxZ + + (0..resolution).forEach outer@ { i -> + val x = if (stepX != 0.0) startX + stepX * i else startX + if (x > endX) return@outer + (0..resolution).forEach inner@ { j -> + val y = if (stepY != 0.0) startY + stepY * j else startY + if (y > endY) return@inner + val z = if (stepZ != 0.0) startZ + stepZ * ((if (stepX != 0.0) j else i)) else startZ + if (z > endZ) return@inner + check(side, Vec3d(x, y, z)) } } + } } /** - * Computes the optimum point by averaging the vectors in a set of points. - */ + * Determines the approximate central point (optimum) of a set of 3D vectors. + */ val Set.optimum: Vec3d? get() = reduceOrNull { acc, vec3d -> acc.add(vec3d) }?.multiply(1.0 / size.toDouble()) + /** + * Determines the sides of a box that are visible from a given position, based on interaction settings. + * + * @param box The box whose visible sides are to be determined. + * @param eye The position (e.g., the player's eyes) to determine visibility from. + * @param interactionSettings The settings that define how visibility checks are handled. + * @return A set of directions corresponding to the visible sides of the box. + */ + fun visibleSides(box: Box, eye: Vec3d, interactionSettings: InteractionConfig) = + if (interactionSettings.visibilityCheck) { + box.getVisibleSurfaces(eye) + } else Direction.entries.toSet() + /** * Gets the bounding coordinates of a box's side, specifying min and max values for each axis. * diff --git a/common/src/main/kotlin/com/lambda/module/HudModule.kt b/common/src/main/kotlin/com/lambda/module/HudModule.kt index c9c68db0d..d51202658 100644 --- a/common/src/main/kotlin/com/lambda/module/HudModule.kt +++ b/common/src/main/kotlin/com/lambda/module/HudModule.kt @@ -87,7 +87,7 @@ abstract class HudModule( rectHandler.screenSize = event.screenSize renderCallables.forEach { function -> - function.invoke(renderer) + function(renderer) } renderer.render() diff --git a/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt similarity index 73% rename from common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt rename to common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt index 62416bf6e..1e741fea7 100644 --- a/common/src/main/kotlin/com/lambda/module/hud/TaskFlow.kt +++ b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt @@ -19,18 +19,21 @@ package com.lambda.module.hud import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag +import com.lambda.task.TaskFlow import com.lambda.util.math.Vec2d -object TaskFlow : HudModule( +object TaskFlowHUD : HudModule( name = "TaskFlowHud", defaultTags = setOf(ModuleTag.CLIENT), ) { - override val width = 50.0 - override val height = 50.0 + override val width = 200.0 + override val height = 200.0 init { onRender { - font.build("TaskFlow", Vec2d.ZERO) + TaskFlow.toString().lines().forEachIndexed { index, line -> + font.build(line, Vec2d(position.x, position.y + index * (font.getHeight(font.scaleMultiplier) + 2.0))) + } } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt index 18403baf5..64e1424c2 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt @@ -18,7 +18,9 @@ package com.lambda.module.modules.client import com.lambda.module.Module +import com.lambda.module.modules.client.GuiSettings.Page import com.lambda.module.tag.ModuleTag +import java.awt.Color object RenderSettings : Module( name = "RenderSettings", @@ -34,6 +36,7 @@ object RenderSettings : Module( val gap by setting("Gap", 1.5, -10.0..10.0, 0.5) { page == Page.Font } val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font } private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font } + val highlightColor by setting("Text Highlight Color", Color(100, 100, 100, 150), visibility = { page == Page.Font }) // ESP val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP } diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt b/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlowModule.kt similarity index 55% rename from common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt rename to common/src/main/kotlin/com/lambda/module/modules/client/TaskFlowModule.kt index ac150e905..9106eb134 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlow.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/TaskFlowModule.kt @@ -18,35 +18,44 @@ package com.lambda.module.modules.client import com.lambda.config.groups.BuildSettings +import com.lambda.config.groups.BuildSettings.Page import com.lambda.config.groups.InteractionSettings +import com.lambda.config.groups.InventorySettings import com.lambda.config.groups.RotationSettings +import com.lambda.event.events.RenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.construction.result.Drawable import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.util.BlockUtils.allSigns import com.lambda.util.item.ItemUtils +import net.minecraft.state.property.Properties -object TaskFlow : Module( +object TaskFlowModule : Module( name = "TaskFlow", description = "Settings for task automation", defaultTags = setOf(ModuleTag.CLIENT, ModuleTag.AUTOMATION) ) { enum class Page { - BUILD, ROTATION, INTERACTION, TASKS + BUILD, ROTATION, INTERACTION, INVENTORY, DEBUG } 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 build = BuildSettings(this) { page == Page.BUILD } + val rotation = RotationSettings(this) { page == Page.ROTATION } + val interact = InteractionSettings(this) { page == Page.INTERACTION } + val inventory = InventorySettings(this) { page == Page.INVENTORY } + + val showAllEntries by setting("Show All Entries", false, "Show all entries in the task tree") { page == Page.DEBUG } + + @Volatile + var drawables = listOf() + + init { + listen { + drawables.toList().forEach { res -> + with(res) { buildRenderer() } + } + } } - val disposables by setting("Disposables", ItemUtils.defaultDisposables) - val ignoredBlocks by setting("Ignored Blocks", allSigns) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt b/common/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt index b09d0948f..95311b4ab 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/combat/KillAura.kt @@ -26,12 +26,13 @@ import com.lambda.event.events.PlayerPacketEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.RotationManager -import com.lambda.interaction.RotationManager.requestRotation +import com.lambda.interaction.RotationManager.rotate import com.lambda.interaction.rotation.Rotation import com.lambda.interaction.rotation.Rotation.Companion.dist import com.lambda.interaction.rotation.Rotation.Companion.rotationTo import com.lambda.interaction.rotation.RotationContext -import com.lambda.interaction.visibilty.VisibilityChecker.scanVisibleSurfaces +import com.lambda.interaction.visibilty.VisibilityChecker.scanSurfaces +import com.lambda.interaction.visibilty.VisibilityChecker.visibleSides import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.threading.runConcurrent @@ -113,15 +114,15 @@ object KillAura : Module( } init { - requestRotation( - onUpdate = { - if (!rotate) return@requestRotation null + rotate { + onUpdate { + if (!rotate) return@onUpdate null target?.let { target -> buildRotation(target) } } - ) + } listen(Int.MIN_VALUE) { event -> prevY = lastY @@ -243,13 +244,15 @@ object KillAura : Module( // Get visible point set val validHits = mutableMapOf() - scanVisibleSurfaces(eye, box, resolution = interactionSettings.resolution) { _, vec -> - if (eye distSq vec > reachSq) return@scanVisibleSurfaces + val sides = visibleSides(box, eye, interactionSettings) + + scanSurfaces(box, sides, resolution = interactionSettings.resolution) { _, vec -> + if (eye distSq vec > reachSq) return@scanSurfaces val newRotation = eye.rotationTo(vec) - val cast = newRotation.rayCast(reach, eye) ?: return@scanVisibleSurfaces - if (cast.entityResult?.entity != target) return@scanVisibleSurfaces + val cast = newRotation.rayCast(reach, eye) ?: return@scanSurfaces + if (cast.entityResult?.entity != target) return@scanSurfaces validHits[vec] = newRotation } 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 index c541a6fdb..719724949 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/debug/ContainerTest.kt @@ -22,6 +22,7 @@ import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.material.StackSelection.Companion.select import com.lambda.module.Module import com.lambda.module.tag.ModuleTag +import com.lambda.task.TaskFlow.run import com.lambda.task.tasks.AcquireMaterial.Companion.acquire import net.minecraft.item.Items @@ -38,7 +39,7 @@ object ContainerTest : Module( onEnable { acquire { Items.OBSIDIAN.select() - }.start(null) + }.run() } } } 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 034e6380a..18e74be3a 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 @@ -18,8 +18,8 @@ package com.lambda.module.modules.debug import com.lambda.Lambda.LOG +import com.lambda.event.events.InventoryEvent import com.lambda.event.events.PacketEvent -import com.lambda.event.events.ScreenHandlerEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.module.Module import com.lambda.module.tag.ModuleTag @@ -35,24 +35,34 @@ object InventoryDebug : Module( defaultTags = setOf(ModuleTag.DEBUG) ) { init { - listen { - info("Opened screen handler: ${it.screenHandler::class.simpleName}") + listen { event -> + info("Opened screen handler: ${event.screenHandler::class.simpleName}") + + LOG.info("\n" + event.screenHandler.slots.joinToString("\n") { + "${it.inventory::class.simpleName} ${it.index} ${it.x} ${it.y}" + }) } - listen { + listen { info("Closed screen handler: ${it.screenHandler::class.simpleName}") } - listen { + listen { info("Updated screen handler: ${it.revision}, ${it.stacks}, ${it.cursorStack}") } listen { - when (val packet = it.packet) { - is UpdateSelectedSlotS2CPacket, is InventoryS2CPacket -> { - this@InventoryDebug.info(packet.dynamicString()) + when (it.packet) { + is UpdateSelectedSlotS2CPacket, + is InventoryS2CPacket + -> { + LOG.info(it.packet.dynamicString()) } } + when (val packet = it.packet) { + is UpdateSelectedSlotS2CPacket -> this@InventoryDebug.info("Updated selected slot: ${packet.slot}") + is InventoryS2CPacket -> this@InventoryDebug.info("Received inventory update: syncId: ${packet.syncId} | revision: ${packet.revision} | cursorStack ${packet.cursorStack}") + } } listen { @@ -64,7 +74,7 @@ object InventoryDebug : Module( is CreativeInventoryActionC2SPacket, is PickFromInventoryC2SPacket, is UpdateSelectedSlotC2SPacket, - -> LOG.info(it.packet.dynamicString()) + -> LOG.info(System.currentTimeMillis().toString() + " " + it.packet.dynamicString()) } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt b/common/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt index 20b7ab900..9982d0575 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/movement/Speed.kt @@ -17,12 +17,12 @@ package com.lambda.module.modules.movement -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.context.SafeContext import com.lambda.event.events.ClientEvent import com.lambda.event.events.MovementEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.interaction.RotationManager.requestRotation +import com.lambda.interaction.RotationManager.rotate import com.lambda.interaction.rotation.Rotation import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.rotation.RotationMode @@ -71,7 +71,7 @@ object Speed : Module( private val ncpTimerBoost by setting("Timer Boost", 1.08, 1.0..1.1, 0.01) { mode == Mode.NCP_STRAFE } // Grim - private val rotationConfig = object : IRotationConfig.Instant { + private val rotationConfig = object : RotationConfig.Instant { override val rotationMode = RotationMode.SYNC } @@ -134,15 +134,15 @@ object Speed : Module( } } - requestRotation(100, alwaysListen = false, - onUpdate = { lastContext -> - if (mode != Mode.GRIM_STRAFE) return@requestRotation null - if (!shouldWork()) return@requestRotation null + rotate(100, alwaysListen = false) { + onUpdate { lastContext -> + if (mode != Mode.GRIM_STRAFE) return@onUpdate null + if (!shouldWork()) return@onUpdate null var yaw = player.yaw val input = newMovementInput() - if (!input.isInputting) return@requestRotation null + if (!input.isInputting) return@onUpdate null run { if (!diagonal) return@run @@ -161,8 +161,8 @@ object Speed : Module( val rotation = Rotation(moveYaw, lastContext?.rotation?.pitch ?: player.pitch.toDouble()) RotationContext(rotation, rotationConfig) - }, {} - ) + } + } onEnable { reset() 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 36901a705..12b6f7456 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 @@ -97,7 +97,7 @@ object PacketLogger : Module( val fileName = "packet-log-${getTime(fileFormatter)}.txt" // ToDo: Organize files with FolderRegister.worldBoundDirectory - file = FolderRegister.packetLogs.resolve(fileName).apply { + file = FolderRegister.packetLogs.resolve(fileName).toFile().apply { if (!parentFile.exists()) { parentFile.mkdirs() } 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 fbb08ceaa..adb76c4a3 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 @@ -18,7 +18,7 @@ package com.lambda.module.modules.player import com.lambda.Lambda.mc -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.event.events.* import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.rotation.Rotation @@ -56,7 +56,7 @@ object Freecam : Module( 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 rotationConfig = object : IRotationConfig.Instant { + private val rotationConfig = object : RotationConfig.Instant { override val rotationMode = RotationMode.LOCK } 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 index b3e9cbee2..809331367 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt @@ -17,36 +17,46 @@ package com.lambda.module.modules.player -import com.lambda.interaction.construction.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.blueprint.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.TaskFlow.run import com.lambda.task.tasks.BuildTask.Companion.build import com.lambda.util.BaritoneUtils import com.lambda.util.Communication.info import com.lambda.util.extension.Structure import com.lambda.util.player.MovementUtils.octant +import com.lambda.util.extension.moveY +import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.math.VecUtils.rotateClockwise 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 height by setting("Height", 4, 2..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 pavement by setting("Pavement", Material.Block, description = "Material for the pavement") + private val rimHeight by setting("Pavement Rim Height", 1, 0..6, 1) { pavement != Material.None } + private val cornerBlock by setting("Corner", Corner.None, description = "Include corner blocks in the highway") { pavement != Material.None } + private val pavementMaterial by setting("Pavement Material", Blocks.OBSIDIAN, description = "Material to build the highway with") { pavement == Material.Block } + private val floor by setting("Floor", Material.None, description = "Material for the floor") + private val floorMaterial by setting("Floor Material", Blocks.NETHERRACK, description = "Material to build the floor with") { floor == Material.Block } + private val walls by setting("Walls", Material.None, description = "Material for the walls") + private val wallMaterial by setting("Wall Material", Blocks.NETHERRACK, description = "Material to build the walls with") { walls == Material.Block } + private val ceiling by setting("Ceiling", Material.None, description = "Material for the ceiling") + private val ceilingMaterial by setting("Ceiling Material", Blocks.OBSIDIAN, description = "Material to build the ceiling with") { ceiling == Material.Block } + private val distance by setting("Distance", -1, -1..1000000, 1, description = "Distance to build the highway/tunnel (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 @@ -54,6 +64,17 @@ object HighwayTools : Module( private var currentPos = BlockPos.ORIGIN private var runningTask: Task<*>? = null + enum class Material { + None, + Solid, + Block, + } + + enum class Corner { + None, + Solid, + } + init { onEnable { octant = player.octant @@ -73,102 +94,138 @@ object HighwayTools : Module( distanceMoved += sliceSize var structure: Structure = mutableMapOf() - val slice = highwaySlice() + val slice = generateSlice() repeat(sliceSize) { val vec = Vec3i(octant.offsetX, 0, octant.offsetZ) currentPos = currentPos.add(vec) structure = structure.plus(slice.map { it.key.add(currentPos) to it.value }) } - runningTask = structure.toBlueprint().build().onSuccess { _, _ -> + runningTask = structure.toBlueprint().build().finally { if (distanceMoved < distance || distance < 0) { buildSlice() } else { this@HighwayTools.info("Highway built") disable() } - }.onFailure { _, _ -> - disable() - }.start(null) + }.run() } - private fun highwaySlice(): Structure { + private fun generateSlice(): Structure { val structure = mutableMapOf() - val orthogonal = EightWayDirection.entries[(octant.ordinal + 2).mod(8)] - val center = (width / 2.0).roundToInt() + val orthogonal = octant.rotateClockwise(2) + val center = (width / 2.0).floorToInt() - // Area to clear + // Hole structure += generateDirectionalTube( orthogonal, width, height, -center, - -1, + 0, ).associateWith { TargetState.Air } - // Highway - structure += generateDirectionalTube( - orthogonal, - width, - 1, - -center, - -1, - ).associateWith { TargetState.Block(material) } + if (pavement != Material.None) { + structure += generateDirectionalTube( + orthogonal, + width, + 1, + -center, + 0, + ).associateWith { target(pavement, pavementMaterial) } - // Left rim - structure += generateDirectionalTube( - orthogonal, - 1, - rimHeight, - -center + width - 1, - 0, - ).associateWith { TargetState.Block(material) } + // Left rim + structure += generateDirectionalTube( + orthogonal, + 1, + rimHeight, + -center + width - 1, + 1, + ).associateWith { target(pavement, pavementMaterial) } - // Right rim - structure += generateDirectionalTube( - orthogonal, - 1, - rimHeight, - -center, - 0, - ).associateWith { TargetState.Block(material) } + // Right rim + structure += generateDirectionalTube( + orthogonal, + 1, + rimHeight, + -center, + 1, + ).associateWith { target(pavement, pavementMaterial) } + + if (cornerBlock == Corner.None) { + // Support for the left rim + structure += generateDirectionalTube( + orthogonal, + 1, + 1, + -center + width - 1, + 0, + ).associateWith { TargetState.Support(Direction.UP) } + + // Support for the right rim + structure += generateDirectionalTube( + orthogonal, + 1, + 1, + -center, + 0, + ).associateWith { TargetState.Support(Direction.UP) } + } + } - if (!cornerBlock) { - structure -= generateDirectionalTube( + if (ceiling != Material.None) { + structure += generateDirectionalTube( orthogonal, + width, 1, + -center, + height, + ).associateWith { target(ceiling, ceilingMaterial) } + } + + if (walls != Material.None) { + // Left wall + structure += generateDirectionalTube( + orthogonal, 1, - -center + width - 1, - -1, - ) + height, + -center + width, + 0, + ).associateWith { target(walls, wallMaterial) } - structure -= generateDirectionalTube( + // Right wall + structure += generateDirectionalTube( orthogonal, 1, + height, + -center - 1, + 0, + ).associateWith { target(walls, wallMaterial) } + } + + if (floor != Material.None) { + structure += generateDirectionalTube( + orthogonal, + width, 1, -center, -1, - ) + ).associateWith { target(floor, floorMaterial) } } - // Support for the left corner - structure += generateDirectionalTube( - orthogonal, - 1, - 1, - -center + width - 1, - -1, - ).associateWith { TargetState.Support(Direction.UP) } + val transformed = when { + pavement != Material.None -> structure.moveY(-1) + else -> structure + } - // Support for the right corner - structure += generateDirectionalTube( - orthogonal, - 1, - 1, - -center, - -1, - ).associateWith { TargetState.Support(Direction.UP) } + return transformed + } - return structure + private fun target(target: Material, material: net.minecraft.block.Block): TargetState { + return when (target) { + Material.Solid -> TargetState.Solid + Material.Block -> TargetState.Block(material) + else -> throw IllegalStateException("Invalid material") + } } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt b/common/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt index 609305212..50ec1bd43 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt @@ -18,15 +18,16 @@ package com.lambda.module.modules.player import com.lambda.context.SafeContext +import com.lambda.event.events.InventoryEvent import com.lambda.event.events.PlayerEvent -import com.lambda.event.events.ScreenHandlerEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.task.Task +import com.lambda.task.TaskFlow.run import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock -import com.lambda.task.tasks.OpenContainer.Companion.openContainer -import com.lambda.task.tasks.PlaceContainer.Companion.placeContainer +import com.lambda.task.tasks.OpenContainer +import com.lambda.task.tasks.PlaceContainer import com.lambda.util.item.ItemUtils.shulkerBoxes import com.lambda.util.player.SlotUtils.clickSlot import com.lambda.util.player.SlotUtils.hotbar @@ -59,19 +60,19 @@ object InventoryTweaks : Module( player.closeScreen() - lastPlace = placeContainer(stack).thenRun(null) { _, placePos -> + lastPlace = PlaceContainer(stack).then { placePos -> placedPos = placePos - openContainer(placePos).onSuccess { _, screenHandler -> + OpenContainer(placePos).finally { screenHandler -> lastOpenScreen = screenHandler } - }.start(null) + }.run() } - listen { event -> + listen { event -> if (event.screenHandler != lastOpenScreen) return@listen lastOpenScreen = null placedPos?.let { - lastBreak = breakAndCollectBlock(it).start(null) + lastBreak = breakAndCollectBlock(it).run() placedPos = null } } 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 7964d127d..606d3618f 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 @@ -17,12 +17,13 @@ package com.lambda.module.modules.player -import com.lambda.interaction.construction.Blueprint.Companion.emptyStructure -import com.lambda.interaction.construction.DynamicBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.emptyStructure +import com.lambda.interaction.construction.blueprint.DynamicBlueprint.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.Companion.emptyTask +import com.lambda.task.Task +import com.lambda.task.TaskFlow.run import com.lambda.task.tasks.BuildTask.Companion.build import com.lambda.util.BlockUtils.blockPos import com.lambda.util.BlockUtils.blockState @@ -39,7 +40,7 @@ object Nuker : Module( private val onlyBreakInstant by setting("Only Break Instant", true) private val fillFloor by setting("Fill Floor", false) - private var task = emptyTask() + private var task: Task<*>? = null init { onEnable { @@ -63,16 +64,13 @@ object Nuker : Module( selection } - .build( - pathing = false, - finishOnDone = false, - cancelOnUnsolvable = false - ) - task.start(null) + // ToDo: Add build setting delegates + .build() + task?.run() } onDisable { - task.cancel() + task?.cancel() } // listener { diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index 6e2049e11..f3c6b92ce 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -29,7 +29,7 @@ import com.lambda.interaction.RotationManager import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.visibilty.VisibilityChecker.findRotation import com.lambda.module.Module -import com.lambda.module.modules.client.TaskFlow +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.module.tag.ModuleTag import com.lambda.util.BlockUtils.blockState import com.lambda.util.math.lerp @@ -514,10 +514,10 @@ object PacketMine : Module( } } - listen { + listen { currentMiningBlock.forEach { ctx -> ctx?.apply { - if (it.pos != pos || !isStateBroken(pos.blockState(world), it.state)) return@forEach + if (it.pos != pos || !isStateBroken(pos.blockState(world), it.newState)) return@forEach if (breakType.isPrimary()) { runHandlers(ProgressStage.PacketReceiveBreak, pos, lastValidBestTool) @@ -539,7 +539,7 @@ object PacketMine : Module( lastNonEmptyState?.let { state -> val boxList = state.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) } val rotationContext = - findRotation(boxList, TaskFlow.rotation, TaskFlow.interact, emptySet()) { true } + findRotation(boxList, TaskFlowModule.rotation, TaskFlowModule.interact, emptySet()) { true } rotationContext?.let { context -> it.context = context expectedRotation = context @@ -1382,10 +1382,12 @@ object PacketMine : Module( } private fun SafeContext.packetStartBreak(pos: BlockPos) { - startBreak(pos) - if (packets != PacketMode.Vanilla || doubleBreak) { + if (packets == PacketMode.Grim) { abortBreak(pos) + stopBreak(pos) } + startBreak(pos) + if (packets == PacketMode.NCP) abortBreak(pos) if (packets == PacketMode.Grim || doubleBreak) { stopBreak(pos) } 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 d1e83d558..e41daa91b 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 @@ -19,7 +19,7 @@ package com.lambda.module.modules.player import com.google.gson.* import com.lambda.brigadier.CommandResult -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.context.SafeContext import com.lambda.core.TimerManager import com.lambda.event.EventFlow.lambdaScope @@ -79,7 +79,7 @@ object Replay : Module( 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 lockCamera by setting("Lock Camera", true) - private val rotationConfig = object : IRotationConfig.Instant { + private val rotationConfig = object : RotationConfig.Instant { override val rotationMode = if (lockCamera) RotationMode.LOCK else RotationMode.SYNC } @@ -429,7 +429,7 @@ object Replay : Module( this@Replay.warn("Recording too short. Minimum length: 5 ticks.") return } - val file = FolderRegister.replay.locationBoundDirectory().resolve("$name.json") + val file = FolderRegister.replay.toFile().locationBoundDirectory().resolve("$name.json") lambdaScope.launch(Dispatchers.IO) { file.writeText(gsonCompact.toJson(recording)) diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt b/common/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt index 333295046..a08ea39e0 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt @@ -26,7 +26,7 @@ import com.lambda.graphics.renderer.esp.DirectionMask import com.lambda.graphics.renderer.esp.DirectionMask.buildSideMesh import com.lambda.graphics.renderer.esp.builders.build import com.lambda.interaction.RotationManager.currentRotation -import com.lambda.interaction.RotationManager.requestRotation +import com.lambda.interaction.RotationManager.rotate import com.lambda.interaction.blockplace.PlaceFinder.Companion.buildPlaceInfo import com.lambda.interaction.blockplace.PlaceInfo import com.lambda.interaction.blockplace.PlaceInteraction.placeBlock @@ -36,9 +36,11 @@ import com.lambda.interaction.rotation.Rotation.Companion.dist import com.lambda.interaction.rotation.Rotation.Companion.rotationTo import com.lambda.interaction.rotation.Rotation.Companion.wrap import com.lambda.interaction.rotation.RotationContext -import com.lambda.interaction.visibilty.VisibilityChecker.scanVisibleSurfaces +import com.lambda.interaction.visibilty.VisibilityChecker.getVisibleSurfaces +import com.lambda.interaction.visibilty.VisibilityChecker.scanSurfaces import com.lambda.module.Module import com.lambda.module.modules.client.GuiSettings +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.module.tag.ModuleTag import com.lambda.util.math.MathUtils.floorToInt import com.lambda.util.math.VecUtils.dist @@ -119,15 +121,15 @@ object Scaffold : Module( } init { - requestRotation( - onUpdate = { + rotate { + onUpdate { lastRotation = null - val info = updatePlaceInfo() ?: return@requestRotation null - val rotation = rotate(info) ?: return@requestRotation null + val info = updatePlaceInfo() ?: return@onUpdate null + val rotation = rotate(info) ?: return@onUpdate null RotationContext(rotation, rotationConfig) } - ) + } listen { if (sneakTicks > 0) it.sneak = true @@ -212,15 +214,19 @@ object Scaffold : Module( // Dividing the surface by segments and iterating through them val pointScan = mutableSetOf().apply { - scanVisibleSurfaces( - eyes = eye, - box = Box(info.clickPos), + val box = Box(info.clickPos) + val sides = if (TaskFlowModule.interact.visibilityCheck) { + box.getVisibleSurfaces(eye) + } else Direction.entries.toSet() + scanSurfaces( + box, + sides, resolution = interactionConfig.resolution ) { _, vec -> - if (eye distSq vec > reachSq) return@scanVisibleSurfaces + if (eye distSq vec > reachSq) return@scanSurfaces val rotation = eye.rotationTo(vec) - castRotation(rotation, info) ?: return@scanVisibleSurfaces + castRotation(rotation, info) ?: return@scanSurfaces add(rotation) } diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt b/common/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt new file mode 100644 index 000000000..17aaf0a28 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/WorldEater.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.module.modules.player + +import com.lambda.event.events.RenderEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.graphics.renderer.esp.builders.buildOutline +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.blueprint.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.TaskFlow.run +import com.lambda.task.tasks.BuildTask.Companion.build +import com.lambda.util.BaritoneUtils +import net.minecraft.util.math.BlockBox +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import java.awt.Color + +object WorldEater : Module( + name = "WorldEater", + description = "Eats the world", + 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 pos1 by setting("Position 1", BlockPos(351, 104, 103)) + private val pos2 by setting("Position 2", BlockPos(361, 70, 113)) + private val layerSize by setting("Layer Size", 1, 1..10, 1) + private var runningTask: Task<*>? = null + private var area = BlockBox.create(pos1, pos2) + private val work = mutableListOf() + + init { + onEnable { + area = BlockBox.create(pos1, pos2) + val layerRanges = (area.minY..area.maxY step layerSize).reversed() + work.addAll(layerRanges.mapNotNull { y -> + if (y == area.minY) return@mapNotNull null + BlockBox(area.minX, y - layerSize, area.minZ, area.maxX, y, area.maxZ) + }) + + buildLayer() + } + + onDisable { + runningTask?.cancel() + runningTask = null + work.clear() + BaritoneUtils.cancel() + } + + listen { + it.renderer.buildOutline(Box.enclosing(pos1, pos2), Color.BLUE) + } + } + + private fun buildLayer() { + work.firstOrNull()?.let { box -> + runningTask = build { + box.toStructure(TargetState.Air) + .toBlueprint() + }.finally { + work.removeFirstOrNull() + buildLayer() + }.run() + } ?: disable() + } +} \ 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 8bf724538..182b92c93 100644 --- a/common/src/main/kotlin/com/lambda/task/Task.kt +++ b/common/src/main/kotlin/com/lambda/task/Task.kt @@ -19,112 +19,49 @@ 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.EventFlow.unsubscribe +import com.lambda.event.Muteable import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.module.modules.client.TaskFlow -import com.lambda.threading.runConcurrent -import com.lambda.threading.runGameScheduled +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.threading.runSafe import com.lambda.util.Communication.logError -import com.lambda.util.Communication.warn import com.lambda.util.Nameable -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 net.minecraft.text.Text -import org.apache.commons.lang3.time.DurationFormatUtils -import java.awt.Color +import com.lambda.util.StringUtils.capitalize -/** - * 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 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. - * - * @property delay The delay before the task starts, in milliseconds. - * @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 : 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 +typealias TaskGenerator = SafeContext.(R) -> Task<*> +typealias TaskGeneratorOrNull = SafeContext.(R) -> Task<*>? +typealias TaskGeneratorUnit = SafeContext.(R) -> Unit +abstract class Task : Nameable, Muteable { private 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 + private var state = State.INIT + override val isMuted: Boolean get() = state == State.PAUSED || state == State.INIT var age = 0 + private val depth: Int get() = parent?.depth?.plus(1) ?: 0 + val isCompleted get() = state == State.COMPLETED + val size: Int get() = subTasks.sumOf { it.size } + 1 - private val isWaiting get() = state == State.WAITING - private val isRunning get() = state == State.RUNNING - val isFailed get() = state == State.FAILED - val isCompleted get() = state == State.COMPLETED || state == State.COOLDOWN - private 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) + open var unpausable = false - val syncListeners = Subscriber() - private val concurrentListeners = Subscriber() + private var nextTask: TaskGenerator? = null + private var nextTaskOrNull: TaskGeneratorOrNull? = null + private var onFinish: TaskGeneratorUnit? = null enum class State { - IDLE, + INIT, RUNNING, - WAITING, + PAUSED, CANCELLED, FAILED, - COOLDOWN, - COMPLETED, + COMPLETED; + + val display get() = name.lowercase().capitalize() } init { - listen { - parent?.let { - it.age++ - } - if (++age >= timeout) { - onTimeout(this@Task) - failure(TimeoutException(age, attempted)) - } - } + listen { age++ } } /** @@ -134,98 +71,117 @@ abstract class Task : Nameable { @DslMarker annotation class Ta5kBuilder + /** + * Invoked when the task starts execution. + * + * This method serves as a lifecycle hook and can be overridden to define + * custom behavior or initialization steps necessary before the task begins. + * It provides a `SafeContext` to access relevant context-sensitive properties + * or actions safely. + * + * By default, this method does not contain any logic. Subclasses may override + * it to implement specific functionality, such as logging, resource allocation, + * or preparing preconditions for task execution. + */ @Ta5kBuilder open fun SafeContext.onStart() {} + /** + * This function is called when the task is canceled. + * It can 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 - fun start(parent: Task<*>?, pauseParent: Boolean = true): Task { - executions++ - val owner = parent ?: RootTask - owner.subTasks.add(this) + open fun SafeContext.onCancel() {} - LOG.info("${owner.identifier} started $identifier") - this.parent = owner - if (pauseParent && owner.isRunning && !owner.isRoot) { - LOG.info("$identifier deactivating parent ${owner.identifier}") - owner.deactivate() + /** + * Executes the current task as a subtask of the specified owner task. + * + * This method adds the current task to the owner's subtasks, sets the parent relationship, + * logs the execution details, and invokes the necessary lifecycle hooks. Additionally, + * it manages the state of the parent task and starts any required listeners for execution. + * + * @param owner The parent task that will execute this task as a child. Must not be the same as this task. + * @param pauseParent Defines whether the parent task should be paused during the execution of this task. Defaults to `true`. + * @return The current task instance as a `Task` to support chaining or further configuration. + * @throws IllegalArgumentException if the owner task is the same as the task being executed. + */ + @Ta5kBuilder + fun execute(owner: Task<*>, pauseParent: Boolean = true): Task { + require(owner != this) { "Cannot execute a task as a child of itself" } + owner.subTasks.add(this) + parent = owner + LOG.info("${owner.name} started $name") + if (!unpausable || pauseParent) { + LOG.info("$name deactivating parent ${owner.name}") + if (owner !is TaskFlow) owner.deactivate() } + state = State.RUNNING + runSafe { runCatching { onStart() }.onFailure { failure(it) } } + return this + } - activate() + @Ta5kBuilder + fun success(result: Result) { + unsubscribe() + state = State.COMPLETED runSafe { - onStart(this@Task) - onStart() + executeNextTask(result) } - return this } @Ta5kBuilder - fun activate() { - if (isRunning) return - state = State.RUNNING - startListening() + fun Task.success() { + success(Unit) } @Ta5kBuilder - fun deactivate() { - if (isWaiting) return - state = State.WAITING - stopListening() + fun activate() { + if (state != State.PAUSED) return + state = State.RUNNING } @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) - } - } - } else finish(result) + fun deactivate() { + if (state != State.RUNNING) return + if (unpausable) return + state = State.PAUSED } - 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) + private fun SafeContext.executeNextTask(result: Result) { + nextTask?.let { taskGen -> + val task = taskGen(this, result) + nextTask = null + parent?.let { owner -> task.execute(owner) } + } ?: nextTaskOrNull?.let { taskGen -> + val task = taskGen(this, result) + nextTaskOrNull = null + parent?.let { owner -> task?.execute(owner) } + } ?: run { + onFinish?.invoke(this, result) + parent?.activate() + onFinish = null } - - notifyParent() - LOG.info("$identifier completed successfully after $attempted retries and $executions executions.") } @Ta5kBuilder fun cancel() { - if (state == State.COMPLETED) return - if (state == State.CANCELLED) return - cancelSubTasks() + if (this is TaskFlow) return + if (state == State.COMPLETED || state == State.CANCELLED) return state = State.CANCELLED - stopListening() - runSafe { onCancel() } - LOG.info("$identifier was cancelled") + unsubscribe() } @Ta5kBuilder fun cancelSubTasks() { - subTasks.forEach { - it.cancel() - } + subTasks.forEach { it.cancel() } + } + + fun clear() { + subTasks.forEach { it.clear() } + subTasks.clear() } @Ta5kBuilder @@ -238,26 +194,15 @@ abstract class Task : Nameable { 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 - } - state = State.FAILED - stopListening() - runSafe { onException(this@Task, e) } + unsubscribe() stacktrace.add(this) parent?.failure(e, stacktrace) ?: run { val message = buildString { stacktrace.firstOrNull()?.let { first -> - append("${first.identifier} failed: ${e.message}\n") + append("${first.name} failed: ${e.message}\n") stacktrace.drop(1).forEach { - append(" -> ${it.identifier}\n") + append(" -> ${it.name}\n") } } } @@ -267,301 +212,109 @@ abstract class Task : Nameable { } /** - * 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() {} - - private fun notifyParent() { - parent?.let { par -> - when { - par.isCompleted -> { - LOG.info("$identifier completed parent ${par.identifier}") - par.notifyParent() - } - - !par.isRunning -> { - LOG.info("$identifier reactivated parent ${par.identifier}") - par.activate() - } - } - } - } - - @Ta5kBuilder - private fun reset() { - age = 0 - } - - private fun startListening() { - EventFlow.syncListeners.subscribe(syncListeners) - EventFlow.concurrentListeners.subscribe(concurrentListeners) - } - - private fun stopListening() { - EventFlow.syncListeners.unsubscribe(syncListeners) - EventFlow.concurrentListeners.unsubscribe(concurrentListeners) - } - - /** - * Sets the delay before the task starts. + * Specifies the next task to execute after the current task completes successfully. * - * @param delay The delay in ticks. - * @return This task instance with the updated delay. - */ - @Ta5kBuilder - fun withDelay(delay: Int): Task { - this.delay = delay - return this - } - - /** - * Sets the timeout for a single attempt of the task - * - * @param timeout The timeout in ticks. - * @return This task instance with the updated timeout. - */ - @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. + * This method links the current task to the specified `task`, creating a sequential + * execution flow where the `task` will be executed immediately after the current task. * - * @param maxAttempts The maximum number of attempts. - * @return This task instance with the updated maximum attempts. + * @param task The task that should be executed following the successful completion + * of the current task. + * @return The current task instance (`Task`) to allow method chaining. */ @Ta5kBuilder - fun withMaxAttempts(maxAttempts: Int): Task { - this.tries = maxAttempts + infix fun then(task: Task<*>): Task { + require(task != this) { "Cannot link a task to itself" } + nextTask = { task } return this } /** - * Sets the number of times the task should be repeated. + * Chains multiple tasks to be executed sequentially after the current 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. + * This method establishes an ordered execution flow between tasks, where each task + * in the provided list will trigger the execution of the next task upon completion. + * It effectively links the current task to the first task in the given array + * and ensures that the sequence is executed in order. * - * @param action The action to be performed. - * @return The task instance with the updated start action. + * @param task A vararg list of tasks to be linked sequentially after the current task. + * These tasks are executed in the order they are provided. + * @return The current task instance (`Task`) to allow method chaining. */ @Ta5kBuilder - fun onStart(action: SafeContext.(Task) -> Unit): Task { - this.onStart = action + fun then(vararg task: Task<*>): Task { + (listOf(this) + task).zipWithNext { current, next -> + current then next + } return this } /** - * Sets the action to be performed when the task completes successfully. + * Adds a subsequent task to the current task's execution sequence. * - * @param action The action to be performed. - * @return The task instance with the updated success action. - */ - @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. + * This method specifies the next task to be executed after the current task + * completes successfully. The task is generated dynamically using the provided + * `TaskGenerator`. This allows chaining tasks together in a flexible manner. * - * @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. + * @param taskGenerator A function that generates the next task based on the result + * of the current task. It takes a `SafeContext` and the result + * of type `R` as input and returns a new `Task`. + * @return The current task instance (`Task`) to allow method chaining. */ @Ta5kBuilder - fun thenRun(owner: Task<*>?, action: SafeContext.(Task, Result) -> Task<*>): Task { - this.onSuccess = { task, result -> - action(this, task, result).start(owner) - } + fun then(taskGenerator: TaskGenerator): Task { + require(nextTask == null) { "Cannot link multiple tasks to a single task" } + nextTask = taskGenerator return this } /** - * Sets the action to be performed when the task is retried after a failure or timeout. + * Adds a subsequent task to the current task's execution sequence conditionally. * - * @param action The action to be performed. - * @return The task instance with the updated retry action. - */ - @Ta5kBuilder - fun onRetry(action: SafeContext.(Task) -> Unit): Task { - this.onRetry = action - return this - } - - /** - * Sets the action to be performed when the task times out. + * This method specifies the next task to be executed after the current task + * completes successfully. The next task is generated dynamically using the provided + * [TaskGeneratorOrNull]. This allows chaining tasks together flexibly, + * where the next task is conditionally determined or null. * - * @param action The action to be performed. - * @return The task instance with the updated timeout action. + * @param taskGenerator A function that generates the next task based on the + * result of the current task. It takes a `SafeContext` + * and the result of type `R` as input and returns a new + * `Task` or null if no next task is required. + * @return The current task instance (`Task`) to allow method chaining. */ @Ta5kBuilder - fun onTimeout(action: SafeContext.(Task) -> Unit): Task { - this.onTimeout = action + fun thenOrNull(taskGenerator: TaskGeneratorOrNull): Task { + require(nextTask == null) { "Cannot link multiple tasks to a single task" } + nextTaskOrNull = taskGenerator return this } /** - * Sets the action to be performed when the task encounters an exception. + * Registers a finalization action to be executed after the current task completes. * - * @param action The action to be performed. - * @return The task instance with the updated exception action. - */ - @Ta5kBuilder - fun onFailure(action: SafeContext.(Task, Throwable) -> Unit): Task { - this.onException = action - return this - } - - /** - * Sets the action to be performed each time the task is repeated. + * This method allows specifying a finalization function that runs upon completion + * of the task, regardless of its outcome (success or failure). It is typically used + * for cleanup operations or logging after a task finishes execution. * - * @param action The action to be performed. - * @return The task instance with the updated repeat action. + * @param onFinish The finalization action to be executed. This function receives + * the task's result of type `R` within a `SafeContext`. + * @return The current task instance (`Task`) to allow method chaining. */ @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 { - listen { - action(this@Task) - } - return this - } - - @Ta5kBuilder - fun withName(name: String): Task { - this.name = name + fun finally(onFinish: TaskGeneratorUnit): Task { + require(this.onFinish == null) { "Cannot link multiple finally blocks to a single task" } + this.onFinish = onFinish 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) - } - } - } + override fun toString() = + buildString { appendTaskTree(this@Task) } - @Ta5kBuilder - inline fun buildTaskWithReturn( - crossinline block: SafeContext.() -> R - ) = object : Task() { - override fun SafeContext.onStart() { - try { - success(block()) - } catch (e: Throwable) { - failure(e) - } - } + private fun StringBuilder.appendTaskTree(task: Task<*>, level: Int = 0) { + appendLine("${" ".repeat(level * 4)}${task.name}" + if (task !is TaskFlow) " [${task.state.display}]" else "") + if (!TaskFlowModule.showAllEntries && (task.state == State.COMPLETED || task.state == State.CANCELLED)) return + task.subTasks.forEach { + if (!TaskFlowModule.showAllEntries && task is TaskFlow && (it.state == State.COMPLETED || it.state == State.CANCELLED)) return@forEach + appendTaskTree(it, level + 1) } } - - 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...") - } - - } } diff --git a/common/src/main/kotlin/com/lambda/task/TaskFlow.kt b/common/src/main/kotlin/com/lambda/task/TaskFlow.kt new file mode 100644 index 000000000..5d647bb4c --- /dev/null +++ b/common/src/main/kotlin/com/lambda/task/TaskFlow.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.task + +import com.lambda.threading.runSafe + +object TaskFlow : Task() { + override val name get() = "TaskFlow" + + @Ta5kBuilder + fun Task<*>.run() = this.execute(this@TaskFlow) + + @Ta5kBuilder + fun Task<*>.run(task: TaskGenerator) { + runSafe { + task(Unit).execute(this@run) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/task/TaskResult.kt b/common/src/main/kotlin/com/lambda/task/TaskResult.kt deleted file mode 100644 index a20aa21b5..000000000 --- a/common/src/main/kotlin/com/lambda/task/TaskResult.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.task - -/** - * Represents the result of a task. - * - * A task result can be successful, failed, timed out, or canceled. - * - * @param Result The type of the result value for successful tasks. Covariant type parameter. - */ -sealed class TaskResult { - - /** - * Represents a successful task result. - * - * @param T The type of the result value. - * @property value The result value. - */ - data class Success(val value: T) : TaskResult() - - /** - * Represents a failed task result. - * - * @property throwable The exception that caused the task to fail. - */ - data class Failure(val throwable: Throwable) : TaskResult() - - /** - * Represents a task result that timed out. - * - * @property timeout The timeout duration in milliseconds. - * @property triesUsed The number of attempts made before the task timed out. - */ - data class Timeout(val timeout: Long, val triesUsed: Int) : TaskResult() - - /** - * Represents a cancelled task result. - */ - data object Cancelled : TaskResult() -} diff --git a/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt b/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt index 242b67bf1..0c7e744a3 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/AcquireMaterial.kt @@ -18,20 +18,23 @@ 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.container.ContainerManager +import com.lambda.interaction.material.container.ContainerManager.findContainerWithMaterial import com.lambda.interaction.material.StackSelection import com.lambda.task.Task -class AcquireMaterial( +class AcquireMaterial @Ta5kBuilder constructor( val selection: StackSelection ) : Task() { + override val name: String + get() = "Acquiring $selection" + override fun SafeContext.onStart() { - findContainerWithSelection(selection) + findContainerWithMaterial(selection) ?.withdraw(selection) - ?.onSuccess { _, _ -> + ?.finally { success(selection) - }?.start(this@AcquireMaterial) + }?.execute(this@AcquireMaterial) ?: failure(ContainerManager.NoContainerFound(selection)) // ToDo: Create crafting path } diff --git a/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt b/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt index 383f5ea53..66d010abb 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/BreakBlock.kt @@ -18,16 +18,16 @@ package com.lambda.task.tasks import baritone.api.pathing.goals.GoalBlock -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.config.groups.InteractionConfig 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.listen +import com.lambda.interaction.RotationManager.rotate import com.lambda.interaction.construction.context.BreakContext import com.lambda.interaction.visibilty.VisibilityChecker.lookAtBlock -import com.lambda.module.modules.client.TaskFlow +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.task.Task import com.lambda.util.BaritoneUtils import com.lambda.util.BlockUtils.blockState @@ -43,13 +43,15 @@ import net.minecraft.util.math.Direction class BreakBlock @Ta5kBuilder constructor( private val ctx: BreakContext, - private val rotation: IRotationConfig, - private val interact: InteractionConfig, - private val sides: Set, - private val collectDrop: Boolean, - private val rotate: Boolean, - private val swingHand: Boolean, + private val collectDrop: Boolean = false, + private val rotation: RotationConfig = TaskFlowModule.rotation, + private val interact: InteractionConfig = TaskFlowModule.interact, + private val sides: Set = Direction.entries.toSet(), + private val rotate: Boolean = TaskFlowModule.build.rotateForBreak, + private val swingHand: Boolean = TaskFlowModule.interact.swingHand, ) : Task() { + override val name get() = "Breaking ${ctx.result.blockPos.toShortString()}" + val blockPos: BlockPos get() = ctx.result.blockPos private var beginState: BlockState? = null @@ -69,95 +71,78 @@ class BreakBlock @Ta5kBuilder constructor( success(null) return } + beginState = blockState if (!rotate || ctx.instantBreak) { - breakBlock(ctx.result.side) + hitBlock(ctx.result.side) } } init { - listen { event -> - if (state != State.BREAKING) return@listen - if (!rotate || ctx.instantBreak) return@listen - event.context = lookAtBlock(blockPos, rotation, interact, sides) - } + rotate { + onUpdate { + if (state != State.BREAKING) return@onUpdate null + if (!rotate || ctx.instantBreak) return@onUpdate null - listen { - if (state != State.BREAKING) return@listen - if (!rotate || ctx.instantBreak) return@listen - - isValid = it.context.isValid + lookAtBlock(blockPos, rotation, interact, sides) + } + onReceive { context -> + isValid = context.isValid + } } listen { drop?.let { itemDrop -> if (!world.entities.contains(itemDrop)) { + BaritoneUtils.cancel() success(itemDrop) return@listen } if (player.hotbarAndStorage.none { it.isEmpty }) { player.currentScreenHandler.inventorySlots.firstOrNull { - it.stack.item.block in TaskFlow.disposables + it.stack.item.block in TaskFlowModule.inventory.disposables }?.let { - clickSlot(it.index, 1, SlotActionType.THROW) + clickSlot(it.id, 1, SlotActionType.THROW) } + return@listen } BaritoneUtils.setGoalAndPath(GoalBlock(itemDrop.blockPos)) + return@listen } ?: BaritoneUtils.cancel() if (isValid || !rotate || ctx.instantBreak) { - breakBlock(ctx.result.side) + hitBlock(ctx.result.side) } if (done()) { - state = State.COLLECTING if (!collectDrop) { + BaritoneUtils.cancel() success(null) } } } + // ToDo: Find out when the stack entity is filled with the item listen { if (collectDrop && it.entity is ItemEntity && it.entity.pos.isInRange(blockPos.toCenterPos(), 0.5) - ) { drop = it.entity + state = State.COLLECTING } } } private fun SafeContext.done() = blockState.isAir && !collectDrop - private fun SafeContext.breakBlock(side: Direction) { + private fun SafeContext.hitBlock(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 - ) - } } diff --git a/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt index 017edf2bf..85ab56e2d 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt @@ -18,112 +18,165 @@ package com.lambda.task.tasks import com.lambda.Lambda.LOG +import com.lambda.config.groups.* import com.lambda.context.SafeContext -import com.lambda.event.events.RenderEvent +import com.lambda.event.events.MovementEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.events.RotationEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -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.RotationManager.rotate +import com.lambda.interaction.construction.blueprint.Blueprint +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.blueprint.DynamicBlueprint +import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.context.BreakContext +import com.lambda.interaction.construction.context.PlaceContext import com.lambda.interaction.construction.result.* import com.lambda.interaction.construction.simulation.BuildGoal import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.Simulation.Companion.simulation import com.lambda.interaction.construction.verify.TargetState -import com.lambda.module.modules.client.TaskFlow +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.task.Task import com.lambda.util.BaritoneUtils +import com.lambda.util.BlockUtils +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Communication.info import com.lambda.util.extension.Structure +import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket import net.minecraft.util.math.BlockPos +import java.util.concurrent.ConcurrentLinkedQueue class BuildTask @Ta5kBuilder constructor( private val blueprint: Blueprint, private val finishOnDone: Boolean = true, - private val pathing: Boolean = TaskFlow.build.pathing, - private val stayInRange: Boolean = true, - private val forceSilkTouch: Boolean = false, - val collectDrops: Boolean = TaskFlow.build.collectDrops, + private val collectDrops: Boolean = TaskFlowModule.build.collectDrops, + private val build: BuildConfig = TaskFlowModule.build, + private val rotation: RotationConfig = TaskFlowModule.rotation, + private val interact: InteractionConfig = TaskFlowModule.interact, ) : Task() { - private var previousResults = setOf() - private val placeTimeout = 15 - private val pending = mutableListOf() + override val name: String get() = "Building $blueprint with ${"%.2f".format(placements / (age / 20.0 + 0.001))} p/s" + + private val pendingPlacements = ConcurrentLinkedQueue() + private val pendingBreaks = ConcurrentLinkedQueue() + + private var currentPlacement: PlaceContext? = null + private var placements = 0 + private var breaks = 0 + private var inScope = 0 override fun SafeContext.onStart() { - (blueprint as? DynamicBlueprint)?.create(this) + (blueprint as? DynamicBlueprint)?.create() } init { - listen { - previousResults.filterIsInstance().forEach { res -> - with(res) { buildRenderer() } - } - } - listen { - pending.removeIf { - if (it.age > placeTimeout) { - it.cancel() - true - } else { - it.isCompleted + pendingPlacements.removeIf { + val timeout = (mc.uptimeInTicks - it.placeTick) > build.placeTimeout + if (timeout) { + info("Placement Timeout of ${it.expectedPos.toShortString()}") } + timeout } - (blueprint as? DynamicBlueprint)?.update(this) + currentPlacement?.let { context -> + if (!context.rotation.isValid) return@listen + if (inScope++ < 1) return@listen // ToDo: Should not be needed but timings are wrong + context.place(interact.swingHand) + pendingPlacements.add(context) + currentPlacement = null + inScope = 0 + } + + (blueprint as? DynamicBlueprint)?.update() if (finishOnDone && blueprint.structure.isEmpty()) { failure("Structure is empty") return@listen } + // ToDo: Simulate for each pair player positions that work val results = blueprint.simulate(player.getCameraPosVec(mc.tickDelta)) - previousResults = results + TaskFlowModule.drawables = results.filterIsInstance().plus(pendingPlacements.toList()) val instantResults = results.filterIsInstance() .filter { it.context.instantBreak } .sorted() - .take(TaskFlow.build.breaksPerTick) + .take(build.breaksPerTick) - if (TaskFlow.build.breaksPerTick > 1 && instantResults.isNotEmpty()) { + if (build.breaksPerTick > 1 && instantResults.isNotEmpty()) { instantResults.forEach { - pending.add(it) - it.start(this@BuildTask, pauseParent = false) + it.resolve().execute(this@BuildTask, pauseParent = false) } return@listen } - if (pending.isNotEmpty()) { - return@listen + val resultsWithoutPending = results.filterNot { res -> + val blockedPositions = pendingPlacements.map { it.expectedPos } + res is PlaceResult.Place && res.context.expectedPos in blockedPositions } - - val result = results.minOrNull() ?: return@listen - when { - !result.rank.solvable -> { -// info("Unsolvable: $result") - if (!blueprint.isDone(this)) return@listen - success(Unit) + val result = resultsWithoutPending.minOrNull() ?: return@listen + when (result) { + is BuildResult.Done, + is BuildResult.Ignored, + is BuildResult.Unbreakable, + is BuildResult.Restricted, + is BuildResult.NoPermission -> { + if (finishOnDone) success() } - - result is BuildResult.NotVisible -> { - if (pathing) BaritoneUtils.setGoalAndPath( - BuildGoal(blueprint.simulation()) - ) + is BuildResult.NotVisible, is PlaceResult.NoIntegrity -> { + if (build.pathing) BaritoneUtils.setGoalAndPath(BuildGoal(blueprint.simulation())) } - - result is Navigable -> { - if (pathing) BaritoneUtils.setGoalAndPath(result.goal) + is Navigable -> { + if (build.pathing) BaritoneUtils.setGoalAndPath(result.goal) } + is PlaceResult.Place -> { + if (pendingPlacements.size >= build.maxPendingPlacements) return@listen - else -> { - LOG.info("Resolving: $result") +// if (!result.context.rotation.isValid) { +// currentPlacement = result.context +// return@listen +// } + + currentPlacement = result.context + } + is Resolvable -> { + LOG.info("Resolving: ${result.name}") if (result is BreakResult.Break) { result.collectDrop = collectDrops } - pending.add(result) - result.start(this@BuildTask, pauseParent = result.pausesParent) + result.resolve().execute(this@BuildTask, pauseParent = result.pausesParent) + } + } + } + + rotate { + onUpdate { + if (currentPlacement == null) return@onUpdate null + if (!build.rotateForPlace) return@onUpdate null + currentPlacement?.rotation + } + } + + listen { + val context = currentPlacement ?: return@listen + val hitBlock = context.result.blockPos.blockState(world).block + if (hitBlock in BlockUtils.interactionBlacklist) { + it.input.sneaking = true + } + } + + listen { event -> + val packet = event.packet + if (packet !is BlockUpdateS2CPacket) return@listen + + pendingPlacements.firstOrNull { it.expectedPos == packet.pos }?.let { + if (it.targetState.matches(packet.state, packet.pos, world)) { + pendingPlacements.remove(it) + placements++ } } } @@ -133,72 +186,55 @@ class BuildTask @Ta5kBuilder constructor( @Ta5kBuilder fun build( finishOnDone: Boolean = true, - pathing: Boolean = TaskFlow.build.pathing, - stayInRange: Boolean = true, - forceSilkTouch: Boolean = false, - collectDrops: Boolean = TaskFlow.build.collectDrops, - cancelOnUnsolvable: Boolean = true, + collectDrops: Boolean = TaskFlowModule.build.collectDrops, + build: BuildConfig = TaskFlowModule.build, + rotation: RotationConfig = TaskFlowModule.rotation, + interact: InteractionConfig = TaskFlowModule.interact, blueprint: () -> Blueprint, - ) = BuildTask( - blueprint(), - finishOnDone, - pathing, - stayInRange, - forceSilkTouch, - collectDrops - ) + ) = BuildTask(blueprint(), finishOnDone, collectDrops, build, rotation, interact) @Ta5kBuilder fun Structure.build( finishOnDone: Boolean = true, - pathing: Boolean = TaskFlow.build.pathing, - stayInRange: Boolean = true, - forceSilkTouch: Boolean = false, - collectDrops: Boolean = TaskFlow.build.collectDrops, - cancelOnUnsolvable: Boolean = true, - ) = BuildTask( - toBlueprint(), - finishOnDone, - pathing, - stayInRange, - forceSilkTouch, - collectDrops - ) + collectDrops: Boolean = TaskFlowModule.build.collectDrops, + build: BuildConfig = TaskFlowModule.build, + rotation: RotationConfig = TaskFlowModule.rotation, + interact: InteractionConfig = TaskFlowModule.interact + ) = BuildTask(toBlueprint(), finishOnDone, collectDrops, build, rotation, interact) @Ta5kBuilder fun Blueprint.build( finishOnDone: Boolean = true, - pathing: Boolean = TaskFlow.build.pathing, - stayInRange: Boolean = true, - forceSilkTouch: Boolean = false, - collectDrops: Boolean = TaskFlow.build.collectDrops, - cancelOnUnsolvable: Boolean = true, - ) = BuildTask( - this, - finishOnDone, - pathing, - stayInRange, - forceSilkTouch, - collectDrops - ) + collectDrops: Boolean = TaskFlowModule.build.collectDrops, + build: BuildConfig = TaskFlowModule.build, + rotation: RotationConfig = TaskFlowModule.rotation, + interact: InteractionConfig = TaskFlowModule.interact + ) = BuildTask(this, finishOnDone, collectDrops, build, rotation, interact) @Ta5kBuilder fun breakAndCollectBlock( blockPos: BlockPos, - withSilkTouch: Boolean = false, - stayInRange: Boolean = false, + finishOnDone: Boolean = true, + collectDrops: Boolean = true, + build: BuildConfig = TaskFlowModule.build, + rotation: RotationConfig = TaskFlowModule.rotation, + interact: InteractionConfig = TaskFlowModule.interact ) = BuildTask( blockPos.toStructure(TargetState.Air).toBlueprint(), - forceSilkTouch = withSilkTouch, - stayInRange = stayInRange, - collectDrops = true + finishOnDone, collectDrops, build, rotation, interact ) @Ta5kBuilder fun breakBlock( blockPos: BlockPos, + finishOnDone: Boolean = true, + collectDrops: Boolean = TaskFlowModule.build.collectDrops, + build: BuildConfig = TaskFlowModule.build, + rotation: RotationConfig = TaskFlowModule.rotation, + interact: InteractionConfig = TaskFlowModule.interact ) = BuildTask( - blockPos.toStructure(TargetState.Air).toBlueprint() + blockPos.toStructure(TargetState.Air).toBlueprint(), + finishOnDone, collectDrops, build, rotation, interact ) } } diff --git a/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt deleted file mode 100644 index f6392ed96..000000000 --- a/common/src/main/kotlin/com/lambda/task/tasks/InventoryTask.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.task.tasks - -import com.lambda.context.SafeContext -import com.lambda.event.events.TickEvent -import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.interaction.material.StackSelection -import com.lambda.module.modules.client.TaskFlow -import com.lambda.task.Task -import com.lambda.util.extension.containerSlots -import com.lambda.util.extension.inventorySlots -import com.lambda.util.item.ItemUtils.block -import com.lambda.util.player.SlotUtils -import net.minecraft.screen.ScreenHandler -import net.minecraft.screen.slot.Slot -import net.minecraft.screen.slot.SlotActionType - -class InventoryTask( - val screen: ScreenHandler, - private val selector: StackSelection, - val from: List, - val to: List, - private val closeScreen: Boolean = true -) : Task() { - private val transactions = mutableListOf() - - override fun SafeContext.onStart() { - val selectedFrom = selector.filterSlots(from).filter { it.hasStack() } - val selectedTo = to.filter { it.stack.isEmpty } + to.filter { it.stack.item.block in TaskFlow.disposables } - selectedFrom.zip(selectedTo).forEach { (from, to) -> - transactions.add(SlotUtils.Transaction(to.id, 0, SlotActionType.SWAP)) - transactions.add(SlotUtils.Transaction(from.id, 0, SlotActionType.SWAP)) - - // ToDo: Handle overflow of cursor for PICKUP - } - } - - init { - // ToDo: Needs smart code to move as efficient as possible. - // Also should handle overflow etc. Should be more generic - listen { - val moved = selector.filterSlots(to) - .filter { it.hasStack() } - .sumOf { it.stack.count } >= selector.count - - if (transactions.isEmpty() || moved) { - if (closeScreen) player.closeHandledScreen() - success(Unit) - } - - transactions.removeFirstOrNull()?.click() ?: success(Unit) - } - } - - companion object { - @Ta5kBuilder - fun moveItems( - screen: ScreenHandler, - selection: StackSelection, - from: List, - to: List, - closeScreen: Boolean = true - ) = InventoryTask(screen, selection, from, to, closeScreen) - - @Ta5kBuilder - fun withdraw(screen: ScreenHandler, selection: StackSelection) = - moveItems(screen, selection, screen.containerSlots, screen.inventorySlots) - - @Ta5kBuilder - fun deposit(screen: ScreenHandler, selection: StackSelection) = - moveItems(screen, selection, screen.inventorySlots, screen.containerSlots) - } -} diff --git a/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt b/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt index eba1a36d1..8cd6ff14a 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/OpenContainer.kt @@ -17,13 +17,13 @@ package com.lambda.task.tasks -import com.lambda.config.groups.IRotationConfig +import com.lambda.config.groups.RotationConfig import com.lambda.config.groups.InteractionConfig +import com.lambda.event.events.InventoryEvent import com.lambda.event.events.RotationEvent -import com.lambda.event.events.ScreenHandlerEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.visibilty.VisibilityChecker.lookAtBlock -import com.lambda.module.modules.client.TaskFlow +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.task.Task import com.lambda.util.world.raycast.RayCastUtils.blockResult import net.minecraft.screen.ScreenHandler @@ -31,26 +31,32 @@ import net.minecraft.util.Hand import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Direction -class OpenContainer( +class OpenContainer @Ta5kBuilder constructor( private val blockPos: BlockPos, private val waitForSlotLoad: Boolean = true, - private val rotate: Boolean, - private val rotation: IRotationConfig = TaskFlow.rotation, - private val interact: InteractionConfig = TaskFlow.interact, - private val sides: Set = emptySet(), + private val rotate: Boolean = true, + private val rotation: RotationConfig = TaskFlowModule.rotation, + private val interact: InteractionConfig = TaskFlowModule.interact, + private val sides: Set = Direction.entries.toSet(), ) : Task() { + override val name get() = "${state.description(inScope)} at ${blockPos.toShortString()}" + private var screenHandler: ScreenHandler? = null private var state = State.SCOPING private var inScope = 0 - override var timeout = 50 - enum class State { - SCOPING, OPENING, SLOT_LOADING + SCOPING, OPENING, SLOT_LOADING; + + fun description(inScope: Int) = when (this) { + SCOPING -> "Waiting for scope ($inScope)" + OPENING -> "Opening container" + SLOT_LOADING -> "Waiting for slots to load" + } } init { - listen { + listen { if (state != State.OPENING) return@listen screenHandler = it.screenHandler @@ -59,14 +65,14 @@ class OpenContainer( if (!waitForSlotLoad) success(it.screenHandler) } - listen { + listen { if (screenHandler != it.screenHandler) return@listen state = State.SCOPING screenHandler = null } - listen { + listen { if (state != State.SLOT_LOADING) return@listen screenHandler?.let { @@ -92,13 +98,4 @@ class OpenContainer( } } } - - companion object { - @Ta5kBuilder - fun openContainer( - blockPos: BlockPos, - waitForSlotLoad: Boolean = true, - rotate: Boolean = true, - ) = OpenContainer(blockPos, waitForSlotLoad, rotate) - } } diff --git a/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt b/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt index 6e2c9d293..fc51f44e2 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/PlaceBlock.kt @@ -20,77 +20,121 @@ package com.lambda.task.tasks import com.lambda.Lambda.LOG import com.lambda.config.groups.InteractionConfig 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.events.* import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.construction.context.PlaceContext -import com.lambda.module.modules.client.TaskFlow +import com.lambda.interaction.rotation.Rotation.Companion.rotation +import com.lambda.interaction.rotation.RotationContext +import com.lambda.module.modules.client.TaskFlowModule import com.lambda.task.Task +import com.lambda.util.BlockUtils import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Communication.info import com.lambda.util.Communication.warn import net.minecraft.block.BlockState +import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket +import kotlin.time.DurationUnit +import kotlin.time.toDuration class PlaceBlock @Ta5kBuilder constructor( private val ctx: PlaceContext, - private val rotate: Boolean, - private val interact: InteractionConfig = TaskFlow.interact, - private val waitForConfirmation: Boolean, + private val rotate: Boolean = TaskFlowModule.build.rotateForPlace, + private val interact: InteractionConfig = TaskFlowModule.interact, + private val waitForConfirmation: Boolean = TaskFlowModule.build.placeConfirmation, ) : Task() { - private var beginState: BlockState? = null - private var state = State.ROTATING - private var findOutIfNeeded = false + override val name get() = "${state.description()} ${ctx.targetState} at ${ctx.result.blockPos.toShortString()}" - private val SafeContext.resultingState: BlockState - get() = ctx.resultingPos.blockState(world) + private var beginState: BlockState? = null + private var state = State.INIT + private var primeContext: RotationContext? = null private val SafeContext.matches - get() = ctx.targetState.matches(ctx.resultingPos.blockState(world), ctx.resultingPos, world) + get() = ctx.targetState.matches(ctx.expectedPos.blockState(world), ctx.expectedPos, world) enum class State { - ROTATING, PLACING, CONFIRMING + INIT, PRIME_ROTATION, ROTATING, PLACING, CONFIRMING; + + fun description() = when (this) { + INIT -> "Prepare placing" + PRIME_ROTATION -> "Priming rotation" + ROTATING -> "Rotating" + PLACING -> "Placing" + CONFIRMING -> "Waiting for confirmation" + } } override fun SafeContext.onStart() { + if (ctx.primeDirection == null) { + state = State.ROTATING + } else { + state = State.PRIME_ROTATION + primeContext = RotationContext( + ctx.primeDirection.rotation, + ctx.rotation.config, + ) + } + if (matches) { finish() return } - beginState = resultingState + beginState = ctx.expectedPos.blockState(world) - if (!rotate) { - placeBlock() - } + if (!rotate) placeBlock() } init { listen { event -> - if (state != State.ROTATING) return@listen if (!rotate) return@listen - event.context = ctx.rotation + event.context = if (state == State.PRIME_ROTATION) { + primeContext + } else ctx.rotation } listen { event -> - if (state != State.ROTATING) return@listen if (!rotate) return@listen - if (event.context != ctx.rotation) return@listen if (!event.context.isValid) return@listen - - state = State.PLACING + when (state) { + State.PRIME_ROTATION -> { + if (event.context.rotation != primeContext?.rotation) return@listen + state = State.ROTATING + } + State.ROTATING -> { + if (event.context.rotation != ctx.rotation.rotation) return@listen + if (!event.context.isValid) return@listen + + state = State.PLACING + } + else -> return@listen + } } listen { if (state != State.PLACING) return@listen + if (!matches) placeBlock() else { + if (!waitForConfirmation) finish() + } + } - if (findOutIfNeeded) placeBlock() - findOutIfNeeded = true + listen { + if (state != State.PLACING) return@listen + val hitBlock = ctx.result.blockPos.blockState(world).block + if (hitBlock in BlockUtils.interactionBlacklist) { + it.input.sneaking = true + } } - listen { - if (it.pos != ctx.resultingPos) return@listen + listen { + val packet = it.packet + if (packet !is BlockUpdateS2CPacket) return@listen + if (state != State.CONFIRMING) return@listen + if (packet.pos != ctx.expectedPos) return@listen - if (ctx.targetState.matches(it.state, it.pos, world)) { + if (ctx.targetState.matches(packet.state, packet.pos, world)) { finish() + } else { + info("Packet: State: ${packet.state.block} at ${packet.pos.toShortString()} doesn't match ${ctx.targetState} at ${ctx.expectedPos.toShortString()} (expected ${ctx.expectedState})") + warn("Packet: Waiting for confirmation...") } } } @@ -113,32 +157,20 @@ class PlaceBlock @Ta5kBuilder constructor( state = State.CONFIRMING - if (matches) { - if (!waitForConfirmation) finish() - } + if (!waitForConfirmation && matches) finish() } else { warn("Internal interaction failed with $actionResult") } } - private fun SafeContext.finish() { + private fun finish() { LOG.info( "Placed at ${ ctx.result.blockPos.toShortString() } (${ctx.result.side}) with expecting state ${ ctx.expectedState - } and expecting position at ${ctx.resultingPos.toShortString()}" + } and expecting position at ${ctx.expectedPos.toShortString()}" ) - success(Unit) - } - - companion object { - @Ta5kBuilder - fun placeBlock( - ctx: PlaceContext, - rotate: Boolean = TaskFlow.build.rotateForPlace, - waitForConfirmation: Boolean = TaskFlow.build.placeConfirmation, - interact: InteractionConfig = TaskFlow.interact, - ) = PlaceBlock(ctx, rotate, interact, waitForConfirmation) + success() } } diff --git a/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt b/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt index da3c0bb51..ce35ec009 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt @@ -18,9 +18,8 @@ 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.context.PlaceContext +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint import com.lambda.interaction.construction.result.BuildResult import com.lambda.interaction.construction.result.PlaceResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate @@ -39,34 +38,33 @@ import net.minecraft.util.math.Direction class PlaceContainer @Ta5kBuilder constructor( val stack: ItemStack, ) : Task() { + private val startStack: ItemStack = stack.copy() + override val name: String get() = "Placing container ${startStack.name.string}" + override fun SafeContext.onStart() { val results = BlockPos.iterateOutwards(player.blockPos, 4, 3, 4) .map { it.blockPos } .flatMap { it.blockPos - .toStructure(TargetState.Stack(stack)) + .toStructure(TargetState.Stack(startStack)) .toBlueprint() .simulate(player.getCameraPosVec(mc.tickDelta)) } -// val res = results.sorted() -// res - val succeeds = results.filterIsInstance().filter { - canBeOpened(stack, it.blockPos, it.context.result.side) + canBeOpened(startStack, it.blockPos, it.context.result.side) } val wrongStacks = results.filterIsInstance().filter { - val result = (it.context as? PlaceContext)?.result ?: return@filter false - canBeOpened(stack, it.blockPos, result.side) + canBeOpened(startStack, it.blockPos, it.context.result.side) } (succeeds + wrongStacks).minOrNull()?.let { result -> build { result.blockPos - .toStructure(TargetState.Stack(stack)) + .toStructure(TargetState.Stack(startStack)) .toBlueprint() - }.onSuccess { _, _ -> + }.finally { success(result.blockPos) - }.start(this@PlaceContainer) + }.execute(this@PlaceContainer) } ?: { failure("No valid placement found") } @@ -80,7 +78,6 @@ class PlaceContainer @Ta5kBuilder constructor( Items.ENDER_CHEST -> { !ChestBlock.isChestBlocked(world, blockPos) } - in shulkerBoxes -> { val box = ShulkerEntity .calculateBoundingBox(direction, 0.0f, 0.5f) @@ -88,14 +85,6 @@ class PlaceContainer @Ta5kBuilder constructor( .contract(1.0E-6) world.isSpaceEmpty(box) } - else -> false } - - companion object { - @Ta5kBuilder - fun placeContainer( - stack: ItemStack, - ) = PlaceContainer(stack) - } } diff --git a/common/src/main/kotlin/com/lambda/util/BlockUtils.kt b/common/src/main/kotlin/com/lambda/util/BlockUtils.kt index 119e18ecc..b7b6528ed 100644 --- a/common/src/main/kotlin/com/lambda/util/BlockUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/BlockUtils.kt @@ -58,6 +58,8 @@ object BlockUtils { Blocks.SOUL_CAMPFIRE, Blocks.JUKEBOX, Blocks.NOTE_BLOCK, + Blocks.STRUCTURE_BLOCK, + Blocks.COMMAND_BLOCK, ).apply { addAll(shulkerBlocks) } val signs = setOf( diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index a57946770..90008b00c 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -18,28 +18,43 @@ package com.lambda.util import com.lambda.Lambda.mc +import com.lambda.core.Loadable import com.lambda.util.StringUtils.sanitizeForFilename import java.io.File import java.net.InetSocketAddress +import java.nio.file.Path +import kotlin.io.path.* +import kotlin.math.min /** * The [FolderRegister] object is responsible for managing the directory structure of the application. * * @property minecraft The root directory of the Minecraft client. * @property lambda The directory for the Lambda client, located within the Minecraft directory. - * @property mods The directory for storing mods, located within the Minecraft directory. * @property config The directory for storing configuration files, located within the Lambda directory. * @property packetLogs The directory for storing packet logs, located within the Lambda directory. * @property replay The directory for storing replay files, located within the Lambda directory. */ -object FolderRegister { - val minecraft: File = mc.runDirectory - val lambda: File = File(minecraft, "lambda") - val mods: File = File(minecraft, "mods") - val config: File = File(lambda, "config") - val packetLogs: File = File(lambda, "packet-log") - val replay: File = File(lambda, "replay") - val cache: File = File(lambda, "cache") +object FolderRegister : Loadable { + val minecraft: Path = mc.runDirectory.toPath() + val lambda: Path = minecraft.resolve("lambda") + val config: Path = lambda.resolve("config") + val packetLogs: Path = lambda.resolve("packet-log") + val replay: Path = lambda.resolve("replay") + val cache: Path = lambda.resolve("cache") + val structure: Path = lambda.resolve("structure") + + override fun load(): String { + val folders = listOf(lambda, config, packetLogs, replay, cache, structure) + val createdFolders = folders.mapNotNull { + if (it.notExists()) { + it.createDirectories() + } else null + } + return if (createdFolders.isNotEmpty()) { + "Created directories: ${createdFolders.joinToString { minecraft.parent.relativize(it).toString() }}" + } else "Loaded ${folders.size} directories" + } /** * Ensures the current file exists by creating it if it does not. diff --git a/common/src/main/kotlin/com/lambda/util/VarIntIterator.kt b/common/src/main/kotlin/com/lambda/util/VarIntIterator.kt new file mode 100644 index 000000000..6892c0622 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/VarIntIterator.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util + +import java.util.NoSuchElementException + +class VarIntIterator( + private val bytes: ByteArray, + private val bitsPerEntry: Int = 7, + private val maxGroups: Int = 5 +) : Iterator { + private var index: Int = 0 + + override fun hasNext(): Boolean = index < bytes.size + + override fun next(): Int { + if (!hasNext()) + throw NoSuchElementException("No more elements to read") + + var value = 0 + var bitsRead = 0 + + val groupMask = (1 shl bitsPerEntry) - 1 + val continuationBit = 1 shl bitsPerEntry + + var b: Byte + do { + if (index >= bytes.size) + throw NoSuchElementException("Unexpected end of byte array while reading VarInt") + + b = bytes[index++] + value = value or ((b.toInt() and groupMask) shl bitsRead) + bitsRead += bitsPerEntry + + require(bitsRead <= bitsPerEntry * maxGroups) { "VarInt size cannot exceed $maxGroups bytes" } + } while ((b.toInt() and continuationBit) != 0) + + return value + } +} diff --git a/common/src/main/kotlin/com/lambda/util/extension/Aliases.kt b/common/src/main/kotlin/com/lambda/util/extension/Aliases.kt index f0f67b903..69be059d6 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Aliases.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Aliases.kt @@ -24,3 +24,15 @@ import net.minecraft.util.math.BlockPos typealias CommandBuilder = LiteralArgumentBuilder typealias Structure = Map + +fun Structure.move(offset: BlockPos): Structure = + mapKeys { (pos, _) -> pos.add(offset) } + +fun Structure.moveX(x: Int): Structure = + mapKeys { (pos, _) -> pos.east(x) } + +fun Structure.moveY(y: Int): Structure = + mapKeys { (pos, _) -> pos.up(y) } + +fun Structure.moveZ(z: Int): Structure = + mapKeys { (pos, _) -> pos.south(z) } diff --git a/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt b/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt new file mode 100644 index 000000000..95aff822e --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/extension/Nbt.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util.extension + +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtList + +/** + * Puts a list of integer into the component, this is not the same as an int array + */ +fun NbtCompound.putIntList(key: String, vararg values: Int) { + put(key, values.fold(NbtList()) { list, value -> list.add(NbtInt.of(value)); list }) +} diff --git a/common/src/main/kotlin/com/lambda/util/extension/Screen.kt b/common/src/main/kotlin/com/lambda/util/extension/Screen.kt index f8a2e4edb..76cd17796 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/Screen.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/Screen.kt @@ -18,12 +18,15 @@ package com.lambda.util.extension import net.minecraft.entity.player.PlayerInventory +import net.minecraft.inventory.CraftingInventory +import net.minecraft.inventory.SimpleInventory 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.containerSlots: List get() = slots.filter { it.inventory is SimpleInventory } val ScreenHandler.inventorySlots: List get() = slots.filter { it.inventory is PlayerInventory } +val ScreenHandler.craftingSlots: List get() = slots.filter { it.inventory is CraftingInventory } val ScreenHandler.containerStacks: List get() = containerSlots.map { it.stack } val ScreenHandler.inventoryStacks: List get() = inventorySlots.map { it.stack } diff --git a/common/src/main/kotlin/com/lambda/util/extension/World.kt b/common/src/main/kotlin/com/lambda/util/extension/World.kt index e3bc2a0f6..ffe9c54d3 100644 --- a/common/src/main/kotlin/com/lambda/util/extension/World.kt +++ b/common/src/main/kotlin/com/lambda/util/extension/World.kt @@ -17,15 +17,21 @@ package com.lambda.util.extension -import com.lambda.util.world.FastVector -import com.lambda.util.world.x -import com.lambda.util.world.y -import com.lambda.util.world.z +import com.lambda.Lambda.mc +import com.lambda.util.VarIntIterator +import com.lambda.util.world.* +import net.minecraft.block.Block import net.minecraft.block.BlockState import net.minecraft.block.Blocks +import net.minecraft.datafixer.DataFixTypes import net.minecraft.fluid.FluidState import net.minecraft.fluid.Fluids +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtList +import net.minecraft.registry.RegistryEntryLookup +import net.minecraft.structure.StructureTemplate import net.minecraft.world.World +import kotlin.experimental.and fun World.getBlockState(x: Int, y: Int, z: Int): BlockState { if (isOutOfHeightLimit(y)) return Blocks.VOID_AIR.defaultState @@ -51,3 +57,130 @@ fun World.getFluidState(x: Int, y: Int, z: Int): FluidState { fun World.getBlockState(vec: FastVector): BlockState = getBlockState(vec.x, vec.y, vec.z) fun World.getFluidState(vec: FastVector): FluidState = getFluidState(vec.x, vec.y, vec.z) + +private fun positionFromIndex(width: Int, length: Int, index: Int): FastVector { + val y = index / (width * length) + val remainder = index - (y * width * length) + val z = remainder / width + val x = remainder - z * width + + return fastVectorOf(x, y, z) +} + +fun StructureTemplate.readNbtOrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? = runCatching { readNbt(lookup, nbt) }.exceptionOrNull() + +fun StructureTemplate.readSpongeOrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? = when (nbt.getInt("Version")) { + 1, 2, 3 -> readSpongeV1OrException(lookup, nbt) + else -> IllegalStateException("Invalid sponge schematic version") +} + +fun StructureTemplate.readSchematicOrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? = when (nbt.getString("Materials")) { + "Alpha" -> IllegalStateException("Not implemented, you can help us by contributing to the project") + "Classic" -> IllegalStateException("Method not implemented, you can help us by contributing to the Minecraft Wiki (https://minecraft.wiki/w/Data_values_(Classic))") + "Pocket" -> IllegalStateException("Pocket Edition schematics are not supported") + else -> IllegalStateException("Invalid MCEdit schematic version") +} + +private fun StructureTemplate.readSpongeV1OrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? { + val version = nbt.getInt("DataVersion") + val width = (nbt.getShort("Width") and 0xFFFF.toShort()).toInt() + val height = (nbt.getShort("Height") and 0xFFFF.toShort()).toInt() + val length = (nbt.getShort("Length") and 0xFFFF.toShort()).toInt() + + val metadata = nbt.getCompound("Metadata") + + // If the offset is too far, we simply ignore it + // I think at some point schematica calculated + // the offset based on the distance between the player position + // and the schematic lower corner, so it would fuck up everything + // when you tried to import and build it using Baritone + // val minimumPosition = nbt.getIntArray("Offset") + // .takeIf { it.isNotEmpty() } + // ?.let { fastVectorOf(it[0], it[1], it[2]) } + // ?.takeIf { 274945015809L times 16 < it } ?: 0L + + val palette = nbt.getCompound("Palette") + + val paletteMax = nbt.getInt("PaletteMax") + val newPalette = NbtList() + + if (palette.size != paletteMax) return IllegalStateException("Block palette size does not match the provided size (corrupted?)") + + palette.keys + .sortedBy { palette.getInt(it) } + .forEach { key -> + val resource = key.substringBefore('[') + val blockState = NbtCompound() + + // Why ? + // I know it's supposed to be SNBT, but it cannot be parsed back + key.substringAfter('[') + .substringBefore(']') + .takeIf { it != resource } + ?.split(',') + ?.associate { it.substringBefore('=') to it.substringAfter('=') } + ?.forEach { (key, value) -> blockState.putString(key, value) } + + // Populate the list using the correct indices + newPalette.add(NbtCompound().apply { + putString("Name", resource) + put("Properties", blockState) + }) + } + + val newBlocks = NbtList() + var blockIndex = 0 + VarIntIterator(nbt.getByteArray("BlockData")) + .forEach { blockId -> + val blockpos = positionFromIndex(width, length, blockIndex++) + + newBlocks.add(NbtCompound().apply { + putIntList("pos", blockpos.x, blockpos.y, blockpos.z) + putInt("state", blockId) + }) + } + + // Construct a structure compatible nbt compound + nbt.putIntList("size", width, height, length) + nbt.put("palette", newPalette) + nbt.put("blocks", newBlocks) + nbt.putString("author", metadata.getString("Author")) + + // Fix the data for future versions + DataFixTypes.STRUCTURE.update(mc.dataFixer, nbt, version) + + // Use the StructureTemplate NBT read utils in order to construct the template + return readNbtOrException(lookup, nbt) +} + +private fun StructureTemplate.readSpongeV3OrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? { + // Third revision + // - 3D Biome support + // - Rename Palette to BlockPalette + // - Wordsmithing varint and palette usages + nbt.put("Palette", nbt.getCompound("BlockPalette")) + + return readSpongeV1OrException(lookup, nbt) +} + +fun StructureTemplate.readLitematicaOrException( + lookup: RegistryEntryLookup, + nbt: NbtCompound, +): Throwable? { + return IllegalStateException("Litematica parsing is not implemented") +} diff --git a/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt b/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt index f5ff325dc..653e3e03a 100644 --- a/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/item/ItemUtils.kt @@ -70,6 +70,7 @@ object ItemUtils { val misc = setOf( Items.SHEARS, + Items.FLINT_AND_STEEL, ) val tools = pickaxes + shovels + axes + hoes + swords + misc diff --git a/common/src/main/kotlin/com/lambda/util/math/VecUtils.kt b/common/src/main/kotlin/com/lambda/util/math/VecUtils.kt index 1f50b4546..49467d432 100644 --- a/common/src/main/kotlin/com/lambda/util/math/VecUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/math/VecUtils.kt @@ -21,6 +21,7 @@ import com.lambda.util.math.MathUtils.sq import net.minecraft.entity.Entity import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Direction +import net.minecraft.util.math.EightWayDirection import net.minecraft.util.math.Vec3d import net.minecraft.util.math.Vec3i import kotlin.math.pow @@ -40,6 +41,9 @@ object VecUtils { get() = CENTER + vector.vec3d * 0.5 + fun EightWayDirection.rotateClockwise(steps: Int) = + EightWayDirection.entries[(ordinal + steps) % 8] + fun Vec3d.approximate(other: Vec3d, precision: Double = 2.0E-4): Boolean = (subtract(other) distSq Vec3d.ZERO) > precision.pow(2) infix fun Vec3d.dist(other: Vec3d): Double = sqrt(this distSq other) infix fun Vec3d.dist(other: Vec3i): Double = sqrt(this distSq other) diff --git a/common/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt b/common/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt new file mode 100644 index 000000000..f32b5cb34 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/player/PlayerUtils.kt @@ -0,0 +1,28 @@ +package com.lambda.util.player + +import com.lambda.context.SafeContext +import net.minecraft.client.network.ClientPlayerEntity + +fun SafeContext.copyPlayer(entity: ClientPlayerEntity) = + ClientPlayerEntity( + mc, + world, + mc.networkHandler, + null, + null, + entity.isSneaking, + entity.isSprinting + ).apply { + setPos(entity.x, entity.y, entity.z) + setExperience(entity.experienceProgress, entity.totalExperience, entity.experienceLevel) + pitch = entity.pitch + yaw = entity.yaw + headYaw = entity.headYaw + bodyYaw = entity.bodyYaw + velocity = entity.velocity + movementSpeed = entity.movementSpeed + isSneaking = entity.isSneaking + isSprinting = entity.isSprinting + isSwimming = entity.isSwimming + isOnGround = entity.isOnGround + } \ 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 index 3288dc35c..d9017c807 100644 --- a/common/src/main/kotlin/com/lambda/util/player/SlotUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/player/SlotUtils.kt @@ -18,7 +18,6 @@ package com.lambda.util.player import com.lambda.context.SafeContext -import com.lambda.threading.runSafe import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.item.ItemStack import net.minecraft.screen.slot.SlotActionType @@ -46,15 +45,4 @@ object SlotUtils { ) } - data class Transaction( - val slotId: Int, - val button: Int, - val actionType: SlotActionType, - ) { - fun click() { - runSafe { - clickSlot(slotId, button, actionType) - } - } - } } diff --git a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt index b611d7dfc..2f7365a3b 100644 --- a/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt +++ b/common/src/main/kotlin/com/lambda/util/text/TextDsl.kt @@ -16,6 +16,7 @@ package com.lambda.util.text +import com.lambda.module.modules.client.RenderSettings import net.minecraft.text.* import net.minecraft.util.Identifier import java.awt.Color @@ -208,6 +209,13 @@ inline fun TextBuilder.color(color: Color?, action: TextBuilder.() -> Unit) { withProp(color, { this.color }, { this.color = it }, action) } +@TextDsl +fun TextBuilder.highlighted(value: String) { + color(RenderSettings.highlightColor) { + literal(value) + } +} + /** * Applies the [TextBuilder] [action] with [bold] set * to the provided value (or enabled if no value given). diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener index b10fed80d..e916c247e 100644 --- a/common/src/main/resources/lambda.accesswidener +++ b/common/src/main/resources/lambda.accesswidener @@ -5,6 +5,7 @@ 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; +accessible field net/minecraft/client/MinecraftClient uptimeInTicks J # World accessible field net/minecraft/client/world/ClientWorld entityManager Lnet/minecraft/client/world/ClientEntityManager; diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json index 1cf55dc7c..aac98e584 100644 --- a/common/src/main/resources/lambda.mixins.common.json +++ b/common/src/main/resources/lambda.mixins.common.json @@ -21,6 +21,7 @@ "items.TridentMixin", "network.ClientConnectionMixin", "network.ClientLoginNetworkMixin", + "network.ClientPlayNetworkHandlerMixin", "network.HandshakeC2SPacketMixin", "network.LoginHelloC2SPacketMixin", "network.LoginKeyC2SPacketMixin", @@ -47,7 +48,9 @@ "render.WorldRendererMixin", "world.BlockCollisionSpliteratorMixin", "world.ClientChunkManagerMixin", - "world.ClientWorldMixin" + "world.ClientWorldMixin", + "world.StructureTemplateMixin", + "world.WorldMixin" ], "injectors": { "defaultRequire": 1 diff --git a/fabric/gradle.properties b/fabric/gradle.properties index e846a8f57..db042830a 100644 --- a/fabric/gradle.properties +++ b/fabric/gradle.properties @@ -1 +1,18 @@ +# +# Copyright 2024 Lambda +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + loom.platform=fabric diff --git a/forge/gradle.properties b/forge/gradle.properties index 82425854e..9ad88f322 100644 --- a/forge/gradle.properties +++ b/forge/gradle.properties @@ -1 +1,18 @@ +# +# Copyright 2024 Lambda +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + loom.platform=forge diff --git a/gradle.properties b/gradle.properties index 8c6d55bdc..bcdb3613c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,20 @@ +# +# Copyright 2024 Lambda +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + # Lambda https://github.com/lambda-client/lambda modId=lambda modVersion=1.0.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138c..e16f1833c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,20 @@ +# +# Copyright 2024 Lambda +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip diff --git a/settings.gradle.kts b/settings.gradle.kts index ddb224f95..1859d9827 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2024 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + rootProject.name = "Lambda" pluginManagement {