From e54e099a5704c410a5cb0191e77588bd34f661c9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 3 May 2024 05:18:07 +0200 Subject: [PATCH 01/14] Added Replay prototype --- .../mixin/entity/ClientPlayerEntityMixin.java | 5 + .../com/lambda/event/events/MovementEvent.kt | 6 +- .../lambda/module/modules/player/Replay.kt | 203 ++++++++++++++++++ .../kotlin/com/lambda/module/tag/ModuleTag.kt | 1 + 4 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt diff --git a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java index f1f501e60..578b7e3d6 100644 --- a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java +++ b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java @@ -61,6 +61,11 @@ void processMovement(Input input, boolean slowDown, float slowDownFactor) { EventFlow.post(new MovementEvent.InputUpdate(input, slowDown, slowDownFactor)); } + @Redirect(method = "tickMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isSprinting()Z")) + boolean isSprinting(ClientPlayerEntity entity) { + return !EventFlow.post(new MovementEvent.Sprint()).isCanceled(); + } + @Inject(method = "sendMovementPackets", at = @At(value = "HEAD"), cancellable = true) void sendBegin(CallbackInfo ci) { ci.cancel(); diff --git a/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt b/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt index d5036ed21..78ba2e7be 100644 --- a/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt @@ -11,10 +11,12 @@ abstract class MovementEvent : Event { class InputUpdate( val input: Input, - val slowDown: Boolean, - val slowDownFactor: Float, + var slowDown: Boolean, + var slowDownFactor: Float, ) : MovementEvent() + class Sprint : MovementEvent(), ICancellable by Cancellable() + class ClipAtLedge( var clip: Boolean, ) : MovementEvent() 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 new file mode 100644 index 000000000..22cd47682 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -0,0 +1,203 @@ +package com.lambda.module.modules.player + +import com.lambda.config.RotationSettings +import com.lambda.event.events.KeyPressEvent +import com.lambda.event.events.MovementEvent +import com.lambda.event.events.RotationEvent +import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.interaction.rotation.Rotation +import com.lambda.interaction.rotation.RotationContext +import com.lambda.interaction.rotation.RotationMode +import com.lambda.module.Module +import com.lambda.module.modules.player.Replay.MoveInputAction.Companion.toAction +import com.lambda.module.tag.ModuleTag +import com.lambda.util.Communication.info +import com.lambda.util.KeyCode +import com.lambda.util.primitives.extension.rotation +import net.minecraft.client.input.Input +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +object Replay : Module( + name = "Replay", + description = "Replays the last few seconds of gameplay", + defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) +) { + private val record by setting("Record", KeyCode.R) + private val replay by setting("Replay", KeyCode.C) + + private val rotationConfig = RotationSettings(this).apply { + rotationMode = RotationMode.LOCK + } + + private var mode = ReplayMode.INACTIVE + + private val actions = mutableListOf() + private val rotations = mutableListOf() + private val sprints = mutableListOf() + private var actionSnapshot = mutableListOf() + private var rotationSnapshot = mutableListOf() + private var sprintSnapshot = mutableListOf() + + private val duration: Duration + get() = (actions.size * 50L).toDuration(DurationUnit.MILLISECONDS) + + enum class ReplayMode { + INACTIVE, + REPLAY, + RECORD + } + + init { + listener { + if (mc.currentScreen != null && !mc.options.commandKey.isPressed) return@listener + + when (it.key) { + record.key -> handleRecord() + replay.key -> handleReplay() + } + } + + listener { event -> + when (mode) { + ReplayMode.RECORD -> { + val action = event.toAction() + actions.add(action) + } + ReplayMode.REPLAY -> { + actionSnapshot.removeFirstOrNull()?.update(event) ?: run { + mode = ReplayMode.INACTIVE + this@Replay.info("Replay finished.") + } + } + else -> {} + } + } + + listener { + when (mode) { + ReplayMode.REPLAY -> { + rotationSnapshot.removeFirstOrNull()?.let { rot -> + it.context = RotationContext(rot, rotationConfig) + } + } + ReplayMode.RECORD -> { + rotations.add(player.rotation) + } + else -> {} + } + } + + listener { + when (mode) { + ReplayMode.REPLAY -> { + sprintSnapshot.removeFirstOrNull()?.let { sprinting -> + if (!sprinting) { + it.cancel() + } else { + player.isSprinting = true + } + } + } + ReplayMode.RECORD -> { + sprints.add(player.isSprinting) + } + else -> {} + } + } + } + + private fun handleReplay() { + when (mode) { + ReplayMode.REPLAY -> { + mode = ReplayMode.INACTIVE + this@Replay.info("Replay stopped.") + } + + ReplayMode.INACTIVE -> { + mode = ReplayMode.REPLAY + actionSnapshot = actions.toMutableList() + rotationSnapshot = rotations.toMutableList() + sprintSnapshot = sprints.toMutableList() + this@Replay.info("Replay started.") + } + + else -> {} + } + } + + private fun handleRecord() { + when (mode) { + ReplayMode.RECORD -> { + mode = ReplayMode.INACTIVE + this@Replay.info("Recording stopped. Recorded for $duration") + } + + ReplayMode.INACTIVE -> { + if (actions.isNotEmpty()) { + this@Replay.info("Overriding previous recording.") + actions.clear() + rotations.clear() + sprints.clear() + } + this@Replay.info("Recording started.") + mode = ReplayMode.RECORD + } + + else -> {} + } + } + + data class MoveInputAction( + val input: InputAction, + val slowDown: Boolean, + val slowDownFactor: Float + ) { + fun update(event: MovementEvent.InputUpdate) { + event.input.update(input) + event.slowDown = slowDown + event.slowDownFactor = slowDownFactor + } + + companion object { + fun MovementEvent.InputUpdate.toAction() = + MoveInputAction( + InputAction( + input.movementSideways, + input.movementForward, + input.pressingForward, + input.pressingBack, + input.pressingLeft, + input.pressingRight, + input.jumping, + input.sneaking + ), + slowDown, + slowDownFactor + ) + + fun Input.update(input: InputAction) { + movementSideways = input.movementSideways + movementForward = input.movementForward + pressingForward = input.pressingForward + pressingBack = input.pressingBack + pressingLeft = input.pressingLeft + pressingRight = input.pressingRight + jumping = input.jumping + sneaking = input.sneaking + } + } + } + + data class InputAction( + val movementSideways: Float, + val movementForward: Float, + val pressingForward: Boolean, + val pressingBack: Boolean, + val pressingLeft: Boolean, + val pressingRight: Boolean, + val jumping: Boolean, + val sneaking: Boolean + ) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt b/common/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt index 433144616..4e9e43981 100644 --- a/common/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt +++ b/common/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt @@ -28,6 +28,7 @@ class ModuleTag(override val name: String) : Nameable { val NETWORK = ModuleTag("Network") val GRIM = ModuleTag("Grim") val BYPASS = ModuleTag("Bypass") + val AUTOMATION = ModuleTag("Automation") val DEBUG = ModuleTag("Debug") } } \ No newline at end of file From 14db86de7d12dd13d09ce665f6600910c6d28ca9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 3 May 2024 05:31:28 +0200 Subject: [PATCH 02/14] Ideas --- .../lambda/module/modules/player/Replay.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) 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 22cd47682..6dd94d4bf 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 @@ -1,5 +1,6 @@ package com.lambda.module.modules.player +import com.google.gson.annotations.SerializedName import com.lambda.config.RotationSettings import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent @@ -15,17 +16,25 @@ import com.lambda.util.Communication.info import com.lambda.util.KeyCode import com.lambda.util.primitives.extension.rotation import net.minecraft.client.input.Input +import net.minecraft.datafixer.fix.BlockEntitySignTextStrictJsonFix.GSON import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration +// ToDo: Needs more dense data storage (Not using JSON) +// - Use a custom binary format to store the data +// - Actually store the data in a file +// - Implement a way to save and load the data (Commands) +// - Pause and resume the replay and recording object Replay : Module( name = "Replay", description = "Replays the last few seconds of gameplay", defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) ) { private val record by setting("Record", KeyCode.R) - private val replay by setting("Replay", KeyCode.C) + private val play by setting("Play", KeyCode.P) + private val pause by setting("Pause", KeyCode.V) + private val stop by setting("Stop", KeyCode.S) private val rotationConfig = RotationSettings(this).apply { rotationMode = RotationMode.LOCK @@ -55,7 +64,7 @@ object Replay : Module( when (it.key) { record.key -> handleRecord() - replay.key -> handleReplay() + play.key -> handlePlay() } } @@ -108,7 +117,7 @@ object Replay : Module( } } - private fun handleReplay() { + private fun handlePlay() { when (mode) { ReplayMode.REPLAY -> { mode = ReplayMode.INACTIVE @@ -131,6 +140,7 @@ object Replay : Module( when (mode) { ReplayMode.RECORD -> { mode = ReplayMode.INACTIVE + this@Replay.info(GSON.toJson(actions)) this@Replay.info("Recording stopped. Recorded for $duration") } @@ -150,8 +160,11 @@ object Replay : Module( } data class MoveInputAction( + @SerializedName("i") val input: InputAction, + @SerializedName("s") val slowDown: Boolean, + @SerializedName("f") val slowDownFactor: Float ) { fun update(event: MovementEvent.InputUpdate) { @@ -191,13 +204,21 @@ object Replay : Module( } data class InputAction( + @SerializedName("s") val movementSideways: Float, + @SerializedName("f") val movementForward: Float, + @SerializedName("pf") val pressingForward: Boolean, + @SerializedName("pb") val pressingBack: Boolean, + @SerializedName("pl") val pressingLeft: Boolean, + @SerializedName("pr") val pressingRight: Boolean, + @SerializedName("j") val jumping: Boolean, + @SerializedName("sn") val sneaking: Boolean ) } \ No newline at end of file From de836f3102732b75a31dadca346e43f3cf7d07f3 Mon Sep 17 00:00:00 2001 From: Constructor Date: Fri, 3 May 2024 05:36:48 +0200 Subject: [PATCH 03/14] More ideas --- .../main/kotlin/com/lambda/module/modules/player/Replay.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 6dd94d4bf..23e722f90 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 @@ -24,8 +24,10 @@ import kotlin.time.toDuration // ToDo: Needs more dense data storage (Not using JSON) // - Use a custom binary format to store the data // - Actually store the data in a file -// - Implement a way to save and load the data (Commands) +// - Implement a way to save and load the data (Commands?) // - Pause and resume the replay and recording +// - Play n time - Loop mode +// - Record other types of inputs: (Interactions, etc.) object Replay : Module( name = "Replay", description = "Replays the last few seconds of gameplay", @@ -140,7 +142,7 @@ object Replay : Module( when (mode) { ReplayMode.RECORD -> { mode = ReplayMode.INACTIVE - this@Replay.info(GSON.toJson(actions)) +// this@Replay.info(GSON.toJson(actions)) this@Replay.info("Recording stopped. Recorded for $duration") } From 7204a88a68b91798e62270d03942dda7969b31cc Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 4 May 2024 03:18:49 +0200 Subject: [PATCH 04/14] Pausing, loops and sanity checks --- .../mixin/entity/ClientPlayerEntityMixin.java | 2 +- .../kotlin/com/lambda/config/Configurable.kt | 12 +- .../com/lambda/config/InteractionSettings.kt | 4 +- .../com/lambda/config/RotationSettings.kt | 8 +- .../com/lambda/event/events/MovementEvent.kt | 2 +- .../lambda/module/modules/player/Replay.kt | 221 ++++++++++++------ 6 files changed, 159 insertions(+), 90 deletions(-) diff --git a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java index 578b7e3d6..d941ca030 100644 --- a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java +++ b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java @@ -63,7 +63,7 @@ void processMovement(Input input, boolean slowDown, float slowDownFactor) { @Redirect(method = "tickMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isSprinting()Z")) boolean isSprinting(ClientPlayerEntity entity) { - return !EventFlow.post(new MovementEvent.Sprint()).isCanceled(); + return EventFlow.post(new MovementEvent.Sprint(entity.isSprinting())).getSprint(); } @Inject(method = "sendMovementPackets", at = @At(value = "HEAD"), cancellable = true) diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index f73573004..f3649052d 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -242,8 +242,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Byte = 1, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = ByteSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } @@ -267,8 +267,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Double = 1.0, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = DoubleSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } @@ -292,8 +292,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Float = 1f, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = FloatSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } @@ -317,8 +317,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Int = 1, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = IntegerSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } @@ -342,8 +342,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Long = 1, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = LongSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } @@ -367,8 +367,8 @@ abstract class Configurable(configuration: Configuration) : Jsonable, Nameable { range: ClosedRange, step: Short = 1, description: String = "", - visibility: () -> Boolean = { true }, unit: String = "", + visibility: () -> Boolean = { true }, ) = ShortSetting(name, defaultValue, range, step, description, visibility, unit).also { settings.add(it) } diff --git a/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt b/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt index 66a6481a1..85f1b810e 100644 --- a/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/InteractionSettings.kt @@ -7,7 +7,7 @@ class InteractionSettings( c: Configurable, vis: () -> Boolean = { true }, ) : InteractionConfig { - override val reach by c.setting("Reach", 5.0, 0.1..10.0, 0.1, "Players reach / range", vis) - override val resolution by c.setting("Resolution", 10, 1..100, 1, "Raycast resolution", vis) + override val reach by c.setting("Reach", 5.0, 0.1..10.0, 0.1, "Players reach / range", "", vis) + override val resolution by c.setting("Resolution", 10, 1..100, 1, "Raycast resolution", "", vis) override val rayCastMask by c.setting("Raycast Mask", RayCastMask.BOTH, "What to raycast against", vis) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt b/common/src/main/kotlin/com/lambda/config/RotationSettings.kt index 6dce63168..c5b469297 100644 --- a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/RotationSettings.kt @@ -9,11 +9,11 @@ class RotationSettings( vis: () -> Boolean = { true }, ) : IRotationConfig { override var rotationMode by c.setting("Mode", RotationMode.SYNC, "SILENT - server-side rotation, SYNC - server-side rotation; client-side movement, LOCK - Lock camera", vis) - override val keepTicks by c.setting("Keep Rotation", 3, 1..10, 1, "Ticks to keep rotation", vis) - override val resetTicks by c.setting("Reset Rotation", 3, 1..10, 1, "Ticks before rotation is reset", vis) + override val keepTicks by c.setting("Keep Rotation", 3, 1..10, 1, "Ticks to keep rotation", "", vis) + override val resetTicks by c.setting("Reset Rotation", 3, 1..10, 1, "Ticks before rotation is reset", "", vis) - private val r1 by c.setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1, "Rotation Speed 1", vis) - private val r2 by c.setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1, "Rotation Speed 2", vis) + private val r1 by c.setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1, "Rotation Speed 1", "", vis) + private val r2 by c.setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1, "Rotation Speed 2", "", vis) override val turnSpeed get() = Random.nextDouble(r1, r2) diff --git a/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt b/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt index 78ba2e7be..543074557 100644 --- a/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt +++ b/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt @@ -15,7 +15,7 @@ abstract class MovementEvent : Event { var slowDownFactor: Float, ) : MovementEvent() - class Sprint : MovementEvent(), ICancellable by Cancellable() + class Sprint(var sprint: Boolean) : MovementEvent() class ClipAtLedge( var clip: Boolean, 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 23e722f90..3f5ac4918 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -2,6 +2,7 @@ package com.lambda.module.modules.player import com.google.gson.annotations.SerializedName import com.lambda.config.RotationSettings +import com.lambda.core.TimerManager import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RotationEvent @@ -13,53 +14,79 @@ import com.lambda.module.Module import com.lambda.module.modules.player.Replay.MoveInputAction.Companion.toAction import com.lambda.module.tag.ModuleTag import com.lambda.util.Communication.info +import com.lambda.util.Communication.warn import com.lambda.util.KeyCode import com.lambda.util.primitives.extension.rotation import net.minecraft.client.input.Input -import net.minecraft.datafixer.fix.BlockEntitySignTextStrictJsonFix.GSON +import net.minecraft.util.math.Vec3d import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration -// ToDo: Needs more dense data storage (Not using JSON) -// - Use a custom binary format to store the data +// ToDo: +// - Use a custom binary format to store the data (Protobuf / DB?) // - Actually store the data in a file // - Implement a way to save and load the data (Commands?) -// - Pause and resume the replay and recording -// - Play n time - Loop mode // - Record other types of inputs: (Interactions, etc.) object Replay : Module( name = "Replay", - description = "Replays the last few seconds of gameplay", + description = "Replay gameplay action recordings", defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) ) { private val record by setting("Record", KeyCode.R) - private val play by setting("Play", KeyCode.P) - private val pause by setting("Pause", KeyCode.V) - private val stop by setting("Stop", KeyCode.S) + private val play by setting("Play / Pause", KeyCode.C) + private val stop by setting("Stop", KeyCode.X) + private val loop by setting("Loop", false) + private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = "repeats") { loop } + private val cancelOnDerivation by setting("Cancel on derivation", true) + private val derivationThreshold by setting("Derivation threshold", 0.1, 0.1..5.0, 0.1, description = "The threshold for the derivation to cancel the replay.") { cancelOnDerivation } private val rotationConfig = RotationSettings(this).apply { rotationMode = RotationMode.LOCK } - private var mode = ReplayMode.INACTIVE + enum class State { + INACTIVE, + RECORDING, + PAUSED_RECORDING, + PLAYING, + PAUSED_REPLAY + } - private val actions = mutableListOf() - private val rotations = mutableListOf() - private val sprints = mutableListOf() - private var actionSnapshot = mutableListOf() - private var rotationSnapshot = mutableListOf() - private var sprintSnapshot = mutableListOf() + private var state = State.INACTIVE - private val duration: Duration - get() = (actions.size * 50L).toDuration(DurationUnit.MILLISECONDS) + data class Recording( + val movement: MutableList, + val rotation: MutableList, + val sprint: MutableList, + val position: MutableList + ) { + val size: Int + get() = maxOf(movement.size, rotation.size, sprint.size, position.size) + val duration: Duration + get() = (size * TimerManager.tickLength * 1.0).toDuration(DurationUnit.MILLISECONDS) - enum class ReplayMode { - INACTIVE, - REPLAY, - RECORD + fun duplicate() = Recording( + movement.toMutableList(), + rotation.toMutableList(), + sprint.toMutableList(), + position.toMutableList() + ) + + companion object { + fun new() = Recording( + mutableListOf(), + mutableListOf(), + mutableListOf(), + mutableListOf() + ) + } } + private var recording: Recording? = null + private var replay: Recording? = null + private var repeats = 0 + init { listener { if (mc.currentScreen != null && !mc.options.commandKey.isPressed) return@listener @@ -67,96 +94,138 @@ object Replay : Module( when (it.key) { record.key -> handleRecord() play.key -> handlePlay() + stop.key -> handleStop() + else -> {} } } listener { event -> - when (mode) { - ReplayMode.RECORD -> { - val action = event.toAction() - actions.add(action) + when (state) { + State.RECORDING -> { + recording?.let { + it.movement.add(event.toAction()) + it.position.add(player.pos) + } } - ReplayMode.REPLAY -> { - actionSnapshot.removeFirstOrNull()?.update(event) ?: run { - mode = ReplayMode.INACTIVE - this@Replay.info("Replay finished.") + State.PLAYING -> { + replay?.let { + it.position.removeFirstOrNull()?.let { pos -> + val diff = pos.subtract(player.pos).length() + if (diff > 0.001) { + this@Replay.info("Current derivation: ${"%.2f".format(diff)} blocks.") + + if (cancelOnDerivation && diff > derivationThreshold) { + state = State.INACTIVE + this@Replay.info("Replay cancelled due to exceeding derivation threshold.") + return@listener + } + } + } + it.movement.removeFirstOrNull()?.update(event) ?: run { + if (loop && repeats < loops) { + repeats++ + replay = recording?.duplicate() + this@Replay.info("Replay looped. $repeats / $loops") + } else { + state = State.INACTIVE + this@Replay.info("Recording finished after ${recording?.duration}.") + } + } } } else -> {} } } - listener { - when (mode) { - ReplayMode.REPLAY -> { - rotationSnapshot.removeFirstOrNull()?.let { rot -> - it.context = RotationContext(rot, rotationConfig) - } + listener { event -> + when (state) { + State.RECORDING -> { + recording?.rotation?.add(player.rotation) } - ReplayMode.RECORD -> { - rotations.add(player.rotation) + State.PLAYING -> { + replay?.let { + it.rotation.removeFirstOrNull()?.let { rot -> + event.context = RotationContext(rot, rotationConfig) + } + } } else -> {} } } - listener { - when (mode) { - ReplayMode.REPLAY -> { - sprintSnapshot.removeFirstOrNull()?.let { sprinting -> - if (!sprinting) { - it.cancel() - } else { - player.isSprinting = true + listener { event -> + when (state) { + State.RECORDING -> { + recording?.sprint?.add(player.isSprinting) + } + State.PLAYING -> { + replay?.let { + it.sprint.removeFirstOrNull()?.let { sprint -> + event.sprint = sprint + player.isSprinting = sprint } } } - ReplayMode.RECORD -> { - sprints.add(player.isSprinting) - } else -> {} } } } private fun handlePlay() { - when (mode) { - ReplayMode.REPLAY -> { - mode = ReplayMode.INACTIVE - this@Replay.info("Replay stopped.") + when (state) { + State.INACTIVE -> { + recording?.let { + state = State.PLAYING + replay = it.duplicate() + this@Replay.info("Replay started and will take ${it.duration}.") + } ?: run { + this@Replay.warn("No recording to replay.") + } } - - ReplayMode.INACTIVE -> { - mode = ReplayMode.REPLAY - actionSnapshot = actions.toMutableList() - rotationSnapshot = rotations.toMutableList() - sprintSnapshot = sprints.toMutableList() - this@Replay.info("Replay started.") + State.RECORDING -> { + state = State.PAUSED_RECORDING + this@Replay.info("Recording paused.") + } + State.PAUSED_RECORDING -> { + state = State.RECORDING + this@Replay.info("Recording resumed.") + } + State.PLAYING -> { + state = State.PAUSED_REPLAY + this@Replay.info("Replay paused.") + } + State.PAUSED_REPLAY -> { + state = State.PLAYING + this@Replay.info("Replay resumed.") } - - else -> {} } } private fun handleRecord() { - when (mode) { - ReplayMode.RECORD -> { - mode = ReplayMode.INACTIVE -// this@Replay.info(GSON.toJson(actions)) - this@Replay.info("Recording stopped. Recorded for $duration") + when (state) { + State.RECORDING -> { + state = State.INACTIVE + this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") } - - ReplayMode.INACTIVE -> { - if (actions.isNotEmpty()) { - this@Replay.info("Overriding previous recording.") - actions.clear() - rotations.clear() - sprints.clear() - } + State.INACTIVE -> { + recording = Recording.new() + state = State.RECORDING this@Replay.info("Recording started.") - mode = ReplayMode.RECORD } + else -> {} + } + } + private fun handleStop() { + when (state) { + State.RECORDING, State.PAUSED_RECORDING -> { + state = State.INACTIVE + this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") + } + State.PLAYING, State.PAUSED_REPLAY -> { + state = State.INACTIVE + this@Replay.info("Replay stopped.") + } else -> {} } } From 4bc7887453034d073b1cb473b5325f1b6d0e97f3 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 4 May 2024 03:45:37 +0200 Subject: [PATCH 05/14] Checkpoints and command concept --- .../lambda/command/commands/ReplayCommand.kt | 21 +++++++ .../lambda/module/modules/player/Replay.kt | 62 ++++++++++++++----- 2 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt diff --git a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt new file mode 100644 index 000000000..b51e6a7ea --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -0,0 +1,21 @@ +package com.lambda.command.commands + +import com.lambda.command.CommandManager.register +import com.lambda.command.LambdaCommand + +object ReplayCommand : LambdaCommand { + override val name = "replay" + + init { + register(name, "rep") { + // 1. Save current replay to disc with name + // 2. Load replay from disc with name + // 3. Play replay + // 4. Stop replay + // 5. Pause replay + // 6. Resume replay + // 7. Set replay speed + // 8. Set replay position + } + } +} \ No newline at end of file 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 3f5ac4918..8cbe276b9 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 @@ -36,6 +36,8 @@ object Replay : Module( private val record by setting("Record", KeyCode.R) private val play by setting("Play / Pause", KeyCode.C) private val stop by setting("Stop", KeyCode.X) + private val check by setting("Checkpoint", KeyCode.V, description = "Create a checkpoint in the recording.") + private val playCheck by setting("Play checkpoints", KeyCode.B, description = "Replays until the last set checkpoint.") private val loop by setting("Loop", false) private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = "repeats") { loop } private val cancelOnDerivation by setting("Cancel on derivation", true) @@ -50,6 +52,7 @@ object Replay : Module( RECORDING, PAUSED_RECORDING, PLAYING, + PLAYING_CHECKPOINTS, PAUSED_REPLAY } @@ -83,6 +86,7 @@ object Replay : Module( } } + private var checkpoint: Recording? = null private var recording: Recording? = null private var replay: Recording? = null private var repeats = 0 @@ -95,6 +99,8 @@ object Replay : Module( record.key -> handleRecord() play.key -> handlePlay() stop.key -> handleStop() + check.key -> handleCheckpoint() + playCheck.key -> handlePlayCheckpoints() else -> {} } } @@ -107,18 +113,17 @@ object Replay : Module( it.position.add(player.pos) } } - State.PLAYING -> { + State.PLAYING, State.PLAYING_CHECKPOINTS -> { replay?.let { - it.position.removeFirstOrNull()?.let { pos -> + it.position.removeFirstOrNull()?.let a@{ pos -> val diff = pos.subtract(player.pos).length() - if (diff > 0.001) { - this@Replay.info("Current derivation: ${"%.2f".format(diff)} blocks.") + if (diff < 0.001) return@a - if (cancelOnDerivation && diff > derivationThreshold) { - state = State.INACTIVE - this@Replay.info("Replay cancelled due to exceeding derivation threshold.") - return@listener - } + this@Replay.info("Current derivation: ${"%.3f".format(diff)} blocks.") + if (cancelOnDerivation && diff > derivationThreshold) { + state = State.INACTIVE + this@Replay.info("Replay cancelled due to exceeding derivation threshold.") + return@listener } } it.movement.removeFirstOrNull()?.update(event) ?: run { @@ -127,8 +132,15 @@ object Replay : Module( replay = recording?.duplicate() this@Replay.info("Replay looped. $repeats / $loops") } else { - state = State.INACTIVE - this@Replay.info("Recording finished after ${recording?.duration}.") + if (state != State.PLAYING_CHECKPOINTS) { + state = State.INACTIVE + this@Replay.info("Replay finished after ${recording?.duration}.") + return@listener + } + + state = State.RECORDING + recording = checkpoint?.duplicate() + this@Replay.info("Checkpoint replayed. Continued recording...") } } } @@ -142,7 +154,7 @@ object Replay : Module( State.RECORDING -> { recording?.rotation?.add(player.rotation) } - State.PLAYING -> { + State.PLAYING, State.PLAYING_CHECKPOINTS -> { replay?.let { it.rotation.removeFirstOrNull()?.let { rot -> event.context = RotationContext(rot, rotationConfig) @@ -158,7 +170,7 @@ object Replay : Module( State.RECORDING -> { recording?.sprint?.add(player.isSprinting) } - State.PLAYING -> { + State.PLAYING, State.PLAYING_CHECKPOINTS -> { replay?.let { it.sprint.removeFirstOrNull()?.let { sprint -> event.sprint = sprint @@ -190,7 +202,7 @@ object Replay : Module( state = State.RECORDING this@Replay.info("Recording resumed.") } - State.PLAYING -> { + State.PLAYING, State.PLAYING_CHECKPOINTS -> { // ToDo: More general pausing for all states state = State.PAUSED_REPLAY this@Replay.info("Replay paused.") } @@ -222,7 +234,7 @@ object Replay : Module( state = State.INACTIVE this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") } - State.PLAYING, State.PAUSED_REPLAY -> { + State.PLAYING, State.PAUSED_REPLAY, State.PLAYING_CHECKPOINTS -> { state = State.INACTIVE this@Replay.info("Replay stopped.") } @@ -230,6 +242,26 @@ object Replay : Module( } } + private fun handleCheckpoint() { + when (state) { + State.RECORDING -> { + checkpoint = recording?.duplicate() + this@Replay.info("Checkpoint created.") + } + else -> {} + } + } + + private fun handlePlayCheckpoints() { + when (state) { + State.INACTIVE -> { + state = State.PLAYING_CHECKPOINTS + replay = checkpoint?.duplicate() + } + else -> {} + } + } + data class MoveInputAction( @SerializedName("i") val input: InputAction, From fd0f422187da4a326fc5467c8cf68cd6003248c0 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 4 May 2024 03:48:33 +0200 Subject: [PATCH 06/14] Updated thoughts --- .../main/kotlin/com/lambda/command/commands/ReplayCommand.kt | 3 +-- .../main/kotlin/com/lambda/module/modules/player/Replay.kt | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 b51e6a7ea..08b8980af 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -8,14 +8,13 @@ object ReplayCommand : LambdaCommand { init { register(name, "rep") { - // 1. Save current replay to disc with name + // 1. Save current recording / checkpoint to disc with name // 2. Load replay from disc with name // 3. Play replay // 4. Stop replay // 5. Pause replay // 6. Resume replay // 7. Set replay speed - // 8. Set replay position } } } \ No newline at end of file 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 8cbe276b9..1f89e57b3 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 @@ -28,9 +28,10 @@ import kotlin.time.toDuration // - Actually store the data in a file // - Implement a way to save and load the data (Commands?) // - Record other types of inputs: (Interactions, etc.) +// - Fix continue derivation after replaying the checkpoint for spliced runs object Replay : Module( name = "Replay", - description = "Replay gameplay action recordings", + description = "Record gameplay actions and replay them like a TAS.", defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) ) { private val record by setting("Record", KeyCode.R) @@ -174,7 +175,7 @@ object Replay : Module( replay?.let { it.sprint.removeFirstOrNull()?.let { sprint -> event.sprint = sprint - player.isSprinting = sprint + player.isSprinting = sprint // ToDo: Find out why } } } From 87179ca0fd8865dc1da91307286e3bd3f1b92064 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 5 May 2024 00:17:53 +0200 Subject: [PATCH 07/14] Cleanup --- .../lambda/module/modules/player/Replay.kt | 148 ++++++++---------- 1 file changed, 64 insertions(+), 84 deletions(-) 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 1f89e57b3..2bc7be83d 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -2,6 +2,7 @@ package com.lambda.module.modules.player import com.google.gson.annotations.SerializedName import com.lambda.config.RotationSettings +import com.lambda.context.SafeContext import com.lambda.core.TimerManager import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent @@ -11,7 +12,7 @@ import com.lambda.interaction.rotation.Rotation import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.rotation.RotationMode import com.lambda.module.Module -import com.lambda.module.modules.player.Replay.MoveInputAction.Companion.toAction +import com.lambda.module.modules.player.Replay.InputAction.Companion.toAction import com.lambda.module.tag.ModuleTag import com.lambda.util.Communication.info import com.lambda.util.Communication.warn @@ -28,7 +29,7 @@ import kotlin.time.toDuration // - Actually store the data in a file // - Implement a way to save and load the data (Commands?) // - Record other types of inputs: (Interactions, etc.) -// - Fix continue derivation after replaying the checkpoint for spliced runs +// - Fix continue deviation after replaying the checkpoint for spliced runs object Replay : Module( name = "Replay", description = "Record gameplay actions and replay them like a TAS.", @@ -37,12 +38,12 @@ object Replay : Module( private val record by setting("Record", KeyCode.R) private val play by setting("Play / Pause", KeyCode.C) private val stop by setting("Stop", KeyCode.X) - private val check by setting("Checkpoint", KeyCode.V, description = "Create a checkpoint in the recording.") - private val playCheck by setting("Play checkpoints", KeyCode.B, description = "Replays until the last set checkpoint.") + private val check by setting("Checkpoint", KeyCode.V, description = "Create a checkpoint while recording.") + private val playCheck by setting("Play until checkpoint", KeyCode.B, description = "Replays until the last set checkpoint.") private val loop by setting("Loop", false) private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = "repeats") { loop } - private val cancelOnDerivation by setting("Cancel on derivation", true) - private val derivationThreshold by setting("Derivation threshold", 0.1, 0.1..5.0, 0.1, description = "The threshold for the derivation to cancel the replay.") { cancelOnDerivation } + private val cancelOnDeviation by setting("Cancel on deviation", true) + private val deviationThreshold by setting("Deviation threshold", 0.1, 0.1..5.0, 0.1, description = "The threshold for the deviation to cancel the replay.") { cancelOnDeviation } private val rotationConfig = RotationSettings(this).apply { rotationMode = RotationMode.LOCK @@ -59,35 +60,8 @@ object Replay : Module( private var state = State.INACTIVE - data class Recording( - val movement: MutableList, - val rotation: MutableList, - val sprint: MutableList, - val position: MutableList - ) { - val size: Int - get() = maxOf(movement.size, rotation.size, sprint.size, position.size) - val duration: Duration - get() = (size * TimerManager.tickLength * 1.0).toDuration(DurationUnit.MILLISECONDS) - - fun duplicate() = Recording( - movement.toMutableList(), - rotation.toMutableList(), - sprint.toMutableList(), - position.toMutableList() - ) - - companion object { - fun new() = Recording( - mutableListOf(), - mutableListOf(), - mutableListOf(), - mutableListOf() - ) - } - } - private var checkpoint: Recording? = null + private var recording: Recording? = null private var replay: Recording? = null private var repeats = 0 @@ -110,7 +84,7 @@ object Replay : Module( when (state) { State.RECORDING -> { recording?.let { - it.movement.add(event.toAction()) + it.input.add(event.input.toAction()) it.position.add(player.pos) } } @@ -120,16 +94,16 @@ object Replay : Module( val diff = pos.subtract(player.pos).length() if (diff < 0.001) return@a - this@Replay.info("Current derivation: ${"%.3f".format(diff)} blocks.") - if (cancelOnDerivation && diff > derivationThreshold) { + this@Replay.warn("Position deviates from the recording by ${"%.3f".format(diff)} blocks.") + if (cancelOnDeviation && diff > deviationThreshold) { state = State.INACTIVE - this@Replay.info("Replay cancelled due to exceeding derivation threshold.") + this@Replay.warn("Replay cancelled due to exceeding deviation threshold.") return@listener } } - it.movement.removeFirstOrNull()?.update(event) ?: run { + it.input.removeFirstOrNull()?.update(event.input) ?: run { if (loop && repeats < loops) { - repeats++ + if (repeats >= 0) repeats++ replay = recording?.duplicate() this@Replay.info("Replay looped. $repeats / $loops") } else { @@ -190,7 +164,7 @@ object Replay : Module( recording?.let { state = State.PLAYING replay = it.duplicate() - this@Replay.info("Replay started and will take ${it.duration}.") + this@Replay.info("Replay started. ETA: ${it.duration}.") } ?: run { this@Replay.warn("No recording to replay.") } @@ -221,7 +195,7 @@ object Replay : Module( this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") } State.INACTIVE -> { - recording = Recording.new() + recording = Recording() state = State.RECORDING this@Replay.info("Recording started.") } @@ -243,9 +217,14 @@ object Replay : Module( } } - private fun handleCheckpoint() { + private fun SafeContext.handleCheckpoint() { when (state) { State.RECORDING -> { + if (player.velocity != Vec3d(0.0, -0.0784000015258789, 0.0)) { + this@Replay.info("Cannot create checkpoint while moving.") + return + } + checkpoint = recording?.duplicate() this@Replay.info("Checkpoint created.") } @@ -258,53 +237,29 @@ object Replay : Module( State.INACTIVE -> { state = State.PLAYING_CHECKPOINTS replay = checkpoint?.duplicate() + this@Replay.info("Replaying until last set checkpoint. ETA: ${checkpoint?.duration}") } else -> {} } } - data class MoveInputAction( - @SerializedName("i") - val input: InputAction, - @SerializedName("s") - val slowDown: Boolean, - @SerializedName("f") - val slowDownFactor: Float + data class Recording( + val input: MutableList = mutableListOf(), + val rotation: MutableList = mutableListOf(), + val sprint: MutableList = mutableListOf(), + val position: MutableList = mutableListOf() ) { - fun update(event: MovementEvent.InputUpdate) { - event.input.update(input) - event.slowDown = slowDown - event.slowDownFactor = slowDownFactor - } - - companion object { - fun MovementEvent.InputUpdate.toAction() = - MoveInputAction( - InputAction( - input.movementSideways, - input.movementForward, - input.pressingForward, - input.pressingBack, - input.pressingLeft, - input.pressingRight, - input.jumping, - input.sneaking - ), - slowDown, - slowDownFactor - ) + val size: Int + get() = minOf(input.size, rotation.size, sprint.size, position.size) + val duration: Duration + get() = (size * TimerManager.tickLength * 1.0).toDuration(DurationUnit.MILLISECONDS) - fun Input.update(input: InputAction) { - movementSideways = input.movementSideways - movementForward = input.movementForward - pressingForward = input.pressingForward - pressingBack = input.pressingBack - pressingLeft = input.pressingLeft - pressingRight = input.pressingRight - jumping = input.jumping - sneaking = input.sneaking - } - } + fun duplicate() = Recording( + input.take(size).toMutableList(), + rotation.take(size).toMutableList(), + sprint.take(size).toMutableList(), + position.take(size).toMutableList() + ) } data class InputAction( @@ -324,5 +279,30 @@ object Replay : Module( val jumping: Boolean, @SerializedName("sn") val sneaking: Boolean - ) + ) { + fun update(input: Input) { + input.movementSideways = movementSideways + input.movementForward = movementForward + input.pressingForward = pressingForward + input.pressingBack = pressingBack + input.pressingLeft = pressingLeft + input.pressingRight = pressingRight + input.jumping = jumping + input.sneaking = sneaking + } + + companion object { + fun Input.toAction() = + InputAction( + movementSideways, + movementForward, + pressingForward, + pressingBack, + pressingLeft, + pressingRight, + jumping, + sneaking + ) + } + } } \ No newline at end of file From c2cb2c5efaad4d760aa585e1a5f4d994bc2e10ad Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 5 May 2024 01:41:19 +0200 Subject: [PATCH 08/14] Fix deviations by stopping the recording on post event --- .../lambda/module/modules/player/Replay.kt | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) 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 2bc7be83d..661c55732 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 @@ -101,23 +101,7 @@ object Replay : Module( return@listener } } - it.input.removeFirstOrNull()?.update(event.input) ?: run { - if (loop && repeats < loops) { - if (repeats >= 0) repeats++ - replay = recording?.duplicate() - this@Replay.info("Replay looped. $repeats / $loops") - } else { - if (state != State.PLAYING_CHECKPOINTS) { - state = State.INACTIVE - this@Replay.info("Replay finished after ${recording?.duration}.") - return@listener - } - - state = State.RECORDING - recording = checkpoint?.duplicate() - this@Replay.info("Checkpoint replayed. Continued recording...") - } - } + it.input.removeFirstOrNull()?.update(event.input) } } else -> {} @@ -156,6 +140,33 @@ object Replay : Module( else -> {} } } + + listener { + when (state) { + State.PLAYING, State.PLAYING_CHECKPOINTS -> { + replay?.let { + if (it.size != 0) return@listener + + if (loop && repeats < loops) { + if (repeats >= 0) repeats++ + replay = recording?.duplicate() + this@Replay.info("Replay looped. $repeats / $loops") + } else { + if (state != State.PLAYING_CHECKPOINTS) { + state = State.INACTIVE + this@Replay.info("Replay finished after ${recording?.duration}.") + return@listener + } + + state = State.RECORDING + recording = checkpoint?.duplicate() + this@Replay.info("Checkpoint replayed. Continued recording...") + } + } + } + else -> {} + } + } } private fun handlePlay() { From 43a81f9bc3d42d973d6b6e74707b8fa977acf1c9 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 5 May 2024 02:25:29 +0200 Subject: [PATCH 09/14] Save and load replays --- .../lambda/command/commands/ReplayCommand.kt | 29 ++++++ .../lambda/module/modules/player/Replay.kt | 93 ++++++++++++++++++- .../kotlin/com/lambda/util/FolderRegister.kt | 1 + 3 files changed, 120 insertions(+), 3 deletions(-) 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 08b8980af..871f36b79 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -1,7 +1,15 @@ package com.lambda.command.commands +import com.lambda.brigadier.CommandResult +import com.lambda.brigadier.argument.string +import com.lambda.brigadier.argument.value +import com.lambda.brigadier.executeWithResult +import com.lambda.brigadier.get +import com.lambda.brigadier.required import com.lambda.command.CommandManager.register import com.lambda.command.LambdaCommand +import com.lambda.module.modules.player.Replay +import com.lambda.util.FolderRegister object ReplayCommand : LambdaCommand { override val name = "replay" @@ -15,6 +23,27 @@ object ReplayCommand : LambdaCommand { // 5. Pause replay // 6. Resume replay // 7. Set replay speed + required(string("replay name")) { replayName -> + suggests { _, builder -> + FolderRegister.replays.listFiles()?.map { + it.nameWithoutExtension + }?.forEach { + builder.suggest(it) + } + builder.buildFuture() + } + + executeWithResult { + val replayFile = FolderRegister.replays.resolve("${this[replayName].value()}.json") + + if (!replayFile.exists()) { + return@executeWithResult CommandResult.failure("Replay file does not exist") + } + + Replay.loadRecording(replayFile) + CommandResult.success() + } + } } } } \ No newline at end of file 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 661c55732..eb3e85880 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 @@ -1,9 +1,11 @@ package com.lambda.module.modules.player +import com.google.gson.* import com.google.gson.annotations.SerializedName import com.lambda.config.RotationSettings import com.lambda.context.SafeContext import com.lambda.core.TimerManager +import com.lambda.event.EventFlow.lambdaScope import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RotationEvent @@ -15,11 +17,17 @@ import com.lambda.module.Module import com.lambda.module.modules.player.Replay.InputAction.Companion.toAction import com.lambda.module.tag.ModuleTag import com.lambda.util.Communication.info +import com.lambda.util.Communication.logError import com.lambda.util.Communication.warn +import com.lambda.util.FolderRegister import com.lambda.util.KeyCode import com.lambda.util.primitives.extension.rotation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import net.minecraft.client.input.Input import net.minecraft.util.math.Vec3d +import java.io.File +import java.lang.reflect.Type import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -29,7 +37,6 @@ import kotlin.time.toDuration // - Actually store the data in a file // - Implement a way to save and load the data (Commands?) // - Record other types of inputs: (Interactions, etc.) -// - Fix continue deviation after replaying the checkpoint for spliced runs object Replay : Module( name = "Replay", description = "Record gameplay actions and replay them like a TAS.", @@ -66,6 +73,16 @@ object Replay : Module( private var replay: Recording? = null private var repeats = 0 + private val gsonCompact = GsonBuilder() + .registerTypeAdapter(Recording::class.java, Recording()) + .create() + + fun loadRecording(file: File) { + recording = gsonCompact.fromJson(file.readText(), Recording::class.java) + + info("Recording ${file.nameWithoutExtension} loaded. Duration: ${recording?.duration}.") + } + init { listener { if (mc.currentScreen != null && !mc.options.commandKey.isPressed) return@listener @@ -232,11 +249,21 @@ object Replay : Module( when (state) { State.RECORDING -> { if (player.velocity != Vec3d(0.0, -0.0784000015258789, 0.0)) { - this@Replay.info("Cannot create checkpoint while moving.") + this@Replay.logError("Cannot create checkpoint while moving. Try again!") return } checkpoint = recording?.duplicate() + lambdaScope.launch(Dispatchers.IO) { + FolderRegister.replays.mkdirs() + FolderRegister.replays.resolve("checkpoint-${ + mc.currentServerEntry?.address?.replace(":", "_") + }-${ + world.dimensionKey?.value?.path?.replace("/", "_") + }-${ + System.currentTimeMillis() + }.json").writeText(gsonCompact.toJson(checkpoint)) + } this@Replay.info("Checkpoint created.") } else -> {} @@ -259,7 +286,7 @@ object Replay : Module( val rotation: MutableList = mutableListOf(), val sprint: MutableList = mutableListOf(), val position: MutableList = mutableListOf() - ) { + ) : JsonSerializer, JsonDeserializer { val size: Int get() = minOf(input.size, rotation.size, sprint.size, position.size) val duration: Duration @@ -271,6 +298,66 @@ object Replay : Module( sprint.take(size).toMutableList(), position.take(size).toMutableList() ) + + override fun serialize( + src: Recording?, + typeOfSrc: Type?, + context: JsonSerializationContext?, + ): JsonElement = src?.let { recording -> + JsonArray().apply { + repeat(recording.size) { i -> + add(JsonArray().apply { + val inputI = recording.input[i] + add(inputI.movementSideways) + add(inputI.movementForward) + add(inputI.pressingForward) + add(inputI.pressingBack) + add(inputI.pressingLeft) + add(inputI.pressingRight) + add(inputI.jumping) + add(inputI.sneaking) + val rotationI = recording.rotation[i] + add(rotationI.yaw) + add(rotationI.pitch) + add(recording.sprint[i]) + val positionI = recording.position[i] + add(positionI.x) + add(positionI.y) + add(positionI.z) + }) + } + } + } ?: JsonNull.INSTANCE + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Recording = json?.asJsonArray?.let { + val input = mutableListOf() + val rotation = mutableListOf() + val sprint = mutableListOf() + val position = mutableListOf() + + it.forEach { element -> + val array = element.asJsonArray + input.add(InputAction( + array[0].asFloat, + array[1].asFloat, + array[2].asBoolean, + array[3].asBoolean, + array[4].asBoolean, + array[5].asBoolean, + array[6].asBoolean, + array[7].asBoolean + )) + rotation.add(Rotation(array[8].asDouble, array[9].asDouble)) + sprint.add(array[10].asBoolean) + position.add(Vec3d(array[11].asDouble, array[12].asDouble, array[13].asDouble)) + } + + Recording(input, rotation, sprint, position) + } ?: Recording() } data class InputAction( diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 493f26622..eef6b0d2b 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -18,4 +18,5 @@ object FolderRegister { val lambda: File = File(minecraft, "lambda") val config: File = File(lambda, "config") val packetLogs: File = File(lambda, "packet-log") + val replays: File = File(lambda, "replays") } From a3de2954c1f0560120c68c9b17bb6d81d0b4dc68 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 5 May 2024 02:48:54 +0200 Subject: [PATCH 10/14] Cleanup --- .../lambda/command/commands/ReplayCommand.kt | 4 +-- .../lambda/module/modules/player/Replay.kt | 30 +++++-------------- .../kotlin/com/lambda/util/FolderRegister.kt | 2 +- 3 files changed, 11 insertions(+), 25 deletions(-) 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 871f36b79..a83aa2545 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -25,7 +25,7 @@ object ReplayCommand : LambdaCommand { // 7. Set replay speed required(string("replay name")) { replayName -> suggests { _, builder -> - FolderRegister.replays.listFiles()?.map { + FolderRegister.replay.listFiles()?.map { it.nameWithoutExtension }?.forEach { builder.suggest(it) @@ -34,7 +34,7 @@ object ReplayCommand : LambdaCommand { } executeWithResult { - val replayFile = FolderRegister.replays.resolve("${this[replayName].value()}.json") + val replayFile = FolderRegister.replay.resolve("${this[replayName].value()}.json") if (!replayFile.exists()) { return@executeWithResult CommandResult.failure("Replay file does not exist") 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 eb3e85880..e9eab552e 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 @@ -34,8 +34,6 @@ import kotlin.time.toDuration // ToDo: // - Use a custom binary format to store the data (Protobuf / DB?) -// - Actually store the data in a file -// - Implement a way to save and load the data (Commands?) // - Record other types of inputs: (Interactions, etc.) object Replay : Module( name = "Replay", @@ -107,6 +105,7 @@ object Replay : Module( } State.PLAYING, State.PLAYING_CHECKPOINTS -> { replay?.let { + it.input.removeFirstOrNull()?.update(event.input) it.position.removeFirstOrNull()?.let a@{ pos -> val diff = pos.subtract(player.pos).length() if (diff < 0.001) return@a @@ -118,7 +117,6 @@ object Replay : Module( return@listener } } - it.input.removeFirstOrNull()?.update(event.input) } } else -> {} @@ -131,10 +129,8 @@ object Replay : Module( recording?.rotation?.add(player.rotation) } State.PLAYING, State.PLAYING_CHECKPOINTS -> { - replay?.let { - it.rotation.removeFirstOrNull()?.let { rot -> - event.context = RotationContext(rot, rotationConfig) - } + replay?.rotation?.removeFirstOrNull()?.let { rot -> + event.context = RotationContext(rot, rotationConfig) } } else -> {} @@ -147,11 +143,9 @@ object Replay : Module( recording?.sprint?.add(player.isSprinting) } State.PLAYING, State.PLAYING_CHECKPOINTS -> { - replay?.let { - it.sprint.removeFirstOrNull()?.let { sprint -> - event.sprint = sprint - player.isSprinting = sprint // ToDo: Find out why - } + replay?.sprint?.removeFirstOrNull()?.let { sprint -> + event.sprint = sprint + player.isSprinting = sprint } } else -> {} @@ -255,8 +249,8 @@ object Replay : Module( checkpoint = recording?.duplicate() lambdaScope.launch(Dispatchers.IO) { - FolderRegister.replays.mkdirs() - FolderRegister.replays.resolve("checkpoint-${ + FolderRegister.replay.mkdirs() + FolderRegister.replay.resolve("checkpoint-${ mc.currentServerEntry?.address?.replace(":", "_") }-${ world.dimensionKey?.value?.path?.replace("/", "_") @@ -361,21 +355,13 @@ object Replay : Module( } data class InputAction( - @SerializedName("s") val movementSideways: Float, - @SerializedName("f") val movementForward: Float, - @SerializedName("pf") val pressingForward: Boolean, - @SerializedName("pb") val pressingBack: Boolean, - @SerializedName("pl") val pressingLeft: Boolean, - @SerializedName("pr") val pressingRight: Boolean, - @SerializedName("j") val jumping: Boolean, - @SerializedName("sn") val sneaking: Boolean ) { fun update(input: Input) { diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index eef6b0d2b..f91647cb5 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -18,5 +18,5 @@ object FolderRegister { val lambda: File = File(minecraft, "lambda") val config: File = File(lambda, "config") val packetLogs: File = File(lambda, "packet-log") - val replays: File = File(lambda, "replays") + val replay: File = File(lambda, "replay") } From c3c30a9111adbf128576c793b4b733a16106dc41 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 5 May 2024 18:11:13 +0200 Subject: [PATCH 11/14] Save on record stop --- .../lambda/module/modules/player/Replay.kt | 79 ++++++++++++++----- .../main/kotlin/com/lambda/util/Formatting.kt | 5 ++ 2 files changed, 64 insertions(+), 20 deletions(-) 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 e9eab552e..ebca961b8 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 @@ -10,22 +10,32 @@ import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RotationEvent import com.lambda.event.listener.SafeListener.Companion.listener +import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.interaction.rotation.Rotation import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.rotation.RotationMode import com.lambda.module.Module +import com.lambda.module.modules.client.GuiSettings import com.lambda.module.modules.player.Replay.InputAction.Companion.toAction import com.lambda.module.tag.ModuleTag import com.lambda.util.Communication.info import com.lambda.util.Communication.logError import com.lambda.util.Communication.warn import com.lambda.util.FolderRegister +import com.lambda.util.Formatting.asString import com.lambda.util.KeyCode import com.lambda.util.primitives.extension.rotation +import com.lambda.util.text.buildText +import com.lambda.util.text.color +import com.lambda.util.text.literal import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.minecraft.client.input.Input +import net.minecraft.client.sound.PositionedSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.sound.SoundEvents import net.minecraft.util.math.Vec3d +import java.awt.Color import java.io.File import java.lang.reflect.Type import kotlin.time.Duration @@ -33,8 +43,10 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration // ToDo: -// - Use a custom binary format to store the data (Protobuf / DB?) -// - Record other types of inputs: (Interactions, etc.) +// - Record other types of inputs: (place, break, inventory, etc.) +// - Fancy logging +// - Add HUD for recording / replaying info +// - Maybe use a custom binary format to store the data (Protobuf / DB?) object Replay : Module( name = "Replay", description = "Record gameplay actions and replay them like a TAS.", @@ -78,7 +90,12 @@ object Replay : Module( fun loadRecording(file: File) { recording = gsonCompact.fromJson(file.readText(), Recording::class.java) - info("Recording ${file.nameWithoutExtension} loaded. Duration: ${recording?.duration}.") + info(buildText { + literal("Recording ") + color(Color.GRAY) { literal(file.nameWithoutExtension) } + literal(" loaded. Duration: ") + color(Color.GRAY) { literal(recording?.duration.toString()) } + }) } init { @@ -110,10 +127,10 @@ object Replay : Module( val diff = pos.subtract(player.pos).length() if (diff < 0.001) return@a - this@Replay.warn("Position deviates from the recording by ${"%.3f".format(diff)} blocks.") + this@Replay.warn("Position deviates from the recording by ${"%.3f".format(diff)} blocks. Desired position: ${pos.asString(3)}") if (cancelOnDeviation && diff > deviationThreshold) { state = State.INACTIVE - this@Replay.warn("Replay cancelled due to exceeding deviation threshold.") + this@Replay.logError("Replay cancelled due to exceeding deviation threshold.") return@listener } } @@ -165,12 +182,21 @@ object Replay : Module( } else { if (state != State.PLAYING_CHECKPOINTS) { state = State.INACTIVE - this@Replay.info("Replay finished after ${recording?.duration}.") + this@Replay.info(buildText { + literal("Replay finished after ") + color(Color.GRAY) { literal(it.duration.toString()) } + literal(".") + }) return@listener } state = State.RECORDING recording = checkpoint?.duplicate() + mc.soundManager.play( + PositionedSoundInstance.master( + SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f + ) + ) this@Replay.info("Checkpoint replayed. Continued recording...") } } @@ -186,7 +212,7 @@ object Replay : Module( recording?.let { state = State.PLAYING replay = it.duplicate() - this@Replay.info("Replay started. ETA: ${it.duration}.") + this@Replay.info("Replay started. Duration: ${it.duration}.") } ?: run { this@Replay.warn("No recording to replay.") } @@ -210,10 +236,13 @@ object Replay : Module( } } - private fun handleRecord() { + private fun SafeContext.handleRecord() { when (state) { State.RECORDING -> { state = State.INACTIVE + recording?.let { + save(it, "recording") + } this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") } State.INACTIVE -> { @@ -225,10 +254,13 @@ object Replay : Module( } } - private fun handleStop() { + private fun SafeContext.handleStop() { when (state) { State.RECORDING, State.PAUSED_RECORDING -> { state = State.INACTIVE + recording?.let { + save(it, "recording") + } this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") } State.PLAYING, State.PAUSED_REPLAY, State.PLAYING_CHECKPOINTS -> { @@ -248,15 +280,8 @@ object Replay : Module( } checkpoint = recording?.duplicate() - lambdaScope.launch(Dispatchers.IO) { - FolderRegister.replay.mkdirs() - FolderRegister.replay.resolve("checkpoint-${ - mc.currentServerEntry?.address?.replace(":", "_") - }-${ - world.dimensionKey?.value?.path?.replace("/", "_") - }-${ - System.currentTimeMillis() - }.json").writeText(gsonCompact.toJson(checkpoint)) + checkpoint?.let { + save(it, "checkpoint") } this@Replay.info("Checkpoint created.") } @@ -269,17 +294,31 @@ object Replay : Module( State.INACTIVE -> { state = State.PLAYING_CHECKPOINTS replay = checkpoint?.duplicate() - this@Replay.info("Replaying until last set checkpoint. ETA: ${checkpoint?.duration}") + this@Replay.info("Replaying until last set checkpoint. Duration: ${checkpoint?.duration}") } else -> {} } } + private fun SafeContext.save(recording: Recording, name: String) { + lambdaScope.launch(Dispatchers.IO) { + FolderRegister.replay.mkdirs() + FolderRegister.replay.resolve("$name-${ + mc.currentServerEntry?.address?.replace(":", "_") + }-${ + world.dimensionKey?.value?.path?.replace("/", "_") + }-${ + System.currentTimeMillis() + }.json").writeText(gsonCompact.toJson(recording)) + } + } + data class Recording( val input: MutableList = mutableListOf(), val rotation: MutableList = mutableListOf(), val sprint: MutableList = mutableListOf(), - val position: MutableList = mutableListOf() + val position: MutableList = mutableListOf(), +// val interaction: MutableList = mutableListOf() ) : JsonSerializer, JsonDeserializer { val size: Int get() = minOf(input.size, rotation.size, sprint.size, position.size) diff --git a/common/src/main/kotlin/com/lambda/util/Formatting.kt b/common/src/main/kotlin/com/lambda/util/Formatting.kt index 346c8d355..768d6b821 100644 --- a/common/src/main/kotlin/com/lambda/util/Formatting.kt +++ b/common/src/main/kotlin/com/lambda/util/Formatting.kt @@ -10,6 +10,11 @@ object Formatting { val Vec3d.asString: String get() = "(%.2f, %.2f, %.2f)".format(x, y, z) + fun Vec3d.asString(decimals: Int = 2): String { + val format = "%.${decimals}f" + return "(${format.format(x)}, ${format.format(y)}, ${format.format(z)})" + } + fun getTime(formatter: DateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME): String { val localDateTime = LocalDateTime.now() val zoneId = ZoneId.systemDefault() From bca4441dc3aa49c9deba17a06db2bd1bfb91bb06 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 6 May 2024 05:34:30 +0200 Subject: [PATCH 12/14] Better file structure, logging and less strict checkpoint setting --- .../lambda/command/commands/ReplayCommand.kt | 12 +- .../com/lambda/config/RotationSettings.kt | 8 +- .../module/modules/client/GuiSettings.kt | 4 +- .../module/modules/network/PacketLogger.kt | 2 + .../lambda/module/modules/player/Replay.kt | 104 +++++++++++------- .../kotlin/com/lambda/util/FolderRegister.kt | 20 ++++ .../main/kotlin/com/lambda/util/Formatting.kt | 2 +- .../kotlin/com/lambda/util/StringUtils.kt | 24 ++++ 8 files changed, 122 insertions(+), 54 deletions(-) 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 a83aa2545..6f22306fd 100644 --- a/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -10,6 +10,7 @@ import com.lambda.command.CommandManager.register import com.lambda.command.LambdaCommand import com.lambda.module.modules.player.Replay import com.lambda.util.FolderRegister +import com.lambda.util.FolderRegister.listRecursive object ReplayCommand : LambdaCommand { override val name = "replay" @@ -25,16 +26,15 @@ object ReplayCommand : LambdaCommand { // 7. Set replay speed required(string("replay name")) { replayName -> suggests { _, builder -> - FolderRegister.replay.listFiles()?.map { - it.nameWithoutExtension - }?.forEach { - builder.suggest(it) - } + val dir = FolderRegister.replay + dir.listRecursive().forEach { + builder.suggest(it.relativeTo(dir).path) + } builder.buildFuture() } executeWithResult { - val replayFile = FolderRegister.replay.resolve("${this[replayName].value()}.json") + val replayFile = FolderRegister.replay.resolve(this[replayName].value()) if (!replayFile.exists()) { return@executeWithResult CommandResult.failure("Replay file does not exist") diff --git a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt b/common/src/main/kotlin/com/lambda/config/RotationSettings.kt index c5b469297..b5f34c005 100644 --- a/common/src/main/kotlin/com/lambda/config/RotationSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/RotationSettings.kt @@ -2,6 +2,8 @@ package com.lambda.config import com.lambda.interaction.rotation.IRotationConfig import com.lambda.interaction.rotation.RotationMode +import kotlin.math.max +import kotlin.math.min import kotlin.random.Random class RotationSettings( @@ -12,10 +14,10 @@ class RotationSettings( override val keepTicks by c.setting("Keep Rotation", 3, 1..10, 1, "Ticks to keep rotation", "", vis) override val resetTicks by c.setting("Reset Rotation", 3, 1..10, 1, "Ticks before rotation is reset", "", vis) - private val r1 by c.setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1, "Rotation Speed 1", "", vis) - private val r2 by c.setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1, "Rotation Speed 2", "", vis) + var r1 by c.setting("Turn Speed 1", 70.0, 1.0..180.0, 0.1, "Rotation Speed 1", "", vis) + var r2 by c.setting("Turn Speed 2", 110.0, 1.0..180.0, 0.1, "Rotation Speed 2", "", vis) - override val turnSpeed get() = Random.nextDouble(r1, r2) + override val turnSpeed get() = Random.nextDouble(min(r1, r2), max(r1, r2) + 0.01) var speedMultiplier = 1.0 diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index 60c98773f..20f04c5ad 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -15,8 +15,8 @@ object GuiSettings : Module( private val scaleSetting by setting("Scale", 1.0, 0.5..3.0, 0.01, visibility = { page == Page.General }) // Colors - private val primaryColor by setting("Primary Color", Color(130, 200, 255), visibility = { page == Page.Colors }) - private val secondaryColor by setting("Secondary Color", Color(225, 130, 225), visibility = { page == Page.Colors && shade }) + val primaryColor by setting("Primary Color", Color(130, 200, 255), visibility = { page == Page.Colors }) + val secondaryColor by setting("Secondary Color", Color(225, 130, 225), visibility = { page == Page.Colors && shade }) val backgroundColor by setting("Background Color", Color(0, 0, 0, 80), visibility = { page == Page.Colors }) val glow by setting("Glow", true, visibility = { page == Page.Colors }) val shade by setting("Shade Color", true, visibility = { page == Page.Colors }) 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 b8d9022e1..3d8451fc0 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 @@ -79,6 +79,8 @@ object PacketLogger : Module( onEnableUnsafe { val fileName = "packet-log-${getTime(fileFormatter)}.txt" + + // ToDo: Organize files with FolderRegister.worldBoundDirectory file = FolderRegister.packetLogs.resolve(fileName).apply { if (!parentFile.exists()) { parentFile.mkdirs() 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 ebca961b8..fe4b40db1 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 @@ -1,16 +1,15 @@ package com.lambda.module.modules.player import com.google.gson.* -import com.google.gson.annotations.SerializedName import com.lambda.config.RotationSettings import com.lambda.context.SafeContext import com.lambda.core.TimerManager import com.lambda.event.EventFlow.lambdaScope +import com.lambda.event.events.InteractionEvent import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RotationEvent import com.lambda.event.listener.SafeListener.Companion.listener -import com.lambda.gui.impl.clickgui.LambdaClickGui import com.lambda.interaction.rotation.Rotation import com.lambda.interaction.rotation.RotationContext import com.lambda.interaction.rotation.RotationMode @@ -22,8 +21,10 @@ import com.lambda.util.Communication.info import com.lambda.util.Communication.logError import com.lambda.util.Communication.warn import com.lambda.util.FolderRegister +import com.lambda.util.FolderRegister.locationBoundDirectory import com.lambda.util.Formatting.asString import com.lambda.util.KeyCode +import com.lambda.util.StringUtils.sanitizeForFilename import com.lambda.util.primitives.extension.rotation import com.lambda.util.text.buildText import com.lambda.util.text.color @@ -32,12 +33,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.minecraft.client.input.Input import net.minecraft.client.sound.PositionedSoundInstance -import net.minecraft.client.sound.SoundInstance import net.minecraft.sound.SoundEvents import net.minecraft.util.math.Vec3d -import java.awt.Color import java.io.File import java.lang.reflect.Type +import java.time.Instant import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -55,8 +55,8 @@ object Replay : Module( private val record by setting("Record", KeyCode.R) private val play by setting("Play / Pause", KeyCode.C) private val stop by setting("Stop", KeyCode.X) - private val check by setting("Checkpoint", KeyCode.V, description = "Create a checkpoint while recording.") - private val playCheck by setting("Play until checkpoint", KeyCode.B, description = "Replays until the last set checkpoint.") + private val check by setting("Set Checkpoint", KeyCode.V, description = "Create a checkpoint while recording.") + private val playCheck by setting("Play Checkpoint", KeyCode.B, description = "Replays until the last set checkpoint.") private val loop by setting("Loop", false) private val loops by setting("Loops", -1, -1..10, 1, description = "Number of times to loop the replay. -1 for infinite.", unit = "repeats") { loop } private val cancelOnDeviation by setting("Cancel on deviation", true) @@ -64,6 +64,8 @@ object Replay : Module( private val rotationConfig = RotationSettings(this).apply { rotationMode = RotationMode.LOCK + r1 = 1000.0 + r2 = 1001.0 } enum class State { @@ -71,8 +73,9 @@ object Replay : Module( RECORDING, PAUSED_RECORDING, PLAYING, + PAUSED_REPLAY, PLAYING_CHECKPOINTS, - PAUSED_REPLAY + PAUSED_CHECKPOINTS } private var state = State.INACTIVE @@ -82,6 +85,7 @@ object Replay : Module( private var recording: Recording? = null private var replay: Recording? = null private var repeats = 0 + private val still = Vec3d(0.0, -0.0784000015258789, 0.0) private val gsonCompact = GsonBuilder() .registerTypeAdapter(Recording::class.java, Recording()) @@ -92,9 +96,9 @@ object Replay : Module( info(buildText { literal("Recording ") - color(Color.GRAY) { literal(file.nameWithoutExtension) } + color(GuiSettings.primaryColor) { literal(file.nameWithoutExtension) } literal(" loaded. Duration: ") - color(Color.GRAY) { literal(recording?.duration.toString()) } + color(GuiSettings.primaryColor) { literal(recording?.duration.toString()) } }) } @@ -178,13 +182,16 @@ object Replay : Module( if (loop && repeats < loops) { if (repeats >= 0) repeats++ replay = recording?.duplicate() - this@Replay.info("Replay looped. $repeats / $loops") + this@Replay.info(buildText { + color(GuiSettings.primaryColor) { literal("[$repeats / $loops]") } + literal(" Replay looped.") + }) } else { if (state != State.PLAYING_CHECKPOINTS) { state = State.INACTIVE this@Replay.info(buildText { literal("Replay finished after ") - color(Color.GRAY) { literal(it.duration.toString()) } + color(GuiSettings.primaryColor) { literal(recording?.duration.toString()) } literal(".") }) return@listener @@ -212,26 +219,37 @@ object Replay : Module( recording?.let { state = State.PLAYING replay = it.duplicate() - this@Replay.info("Replay started. Duration: ${it.duration}.") + info(buildText { + literal("Replay started. Duration: ") + color(GuiSettings.primaryColor) { literal(it.duration.toString()) } + }) } ?: run { this@Replay.warn("No recording to replay.") } } State.RECORDING -> { state = State.PAUSED_RECORDING - this@Replay.info("Recording paused.") + info("Recording paused.") } State.PAUSED_RECORDING -> { state = State.RECORDING - this@Replay.info("Recording resumed.") + info("Recording resumed.") } - State.PLAYING, State.PLAYING_CHECKPOINTS -> { // ToDo: More general pausing for all states + State.PLAYING -> { // ToDo: More general pausing for all states state = State.PAUSED_REPLAY - this@Replay.info("Replay paused.") + info("Replay paused.") } State.PAUSED_REPLAY -> { state = State.PLAYING - this@Replay.info("Replay resumed.") + info("Replay resumed.") + } + State.PLAYING_CHECKPOINTS -> { + state = State.PAUSED_CHECKPOINTS + info("Checkpoint replay paused.") + } + State.PAUSED_CHECKPOINTS -> { + state = State.PLAYING_CHECKPOINTS + info("Checkpoint replay resumed.") } } } @@ -239,13 +257,14 @@ object Replay : Module( private fun SafeContext.handleRecord() { when (state) { State.RECORDING -> { - state = State.INACTIVE - recording?.let { - save(it, "recording") - } - this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") + stopRecording() } State.INACTIVE -> { + if (player.velocity != still) { + this@Replay.logError("Cannot start recording while moving. Slow down and try again!") + return + } + recording = Recording() state = State.RECORDING this@Replay.info("Recording started.") @@ -257,11 +276,7 @@ object Replay : Module( private fun SafeContext.handleStop() { when (state) { State.RECORDING, State.PAUSED_RECORDING -> { - state = State.INACTIVE - recording?.let { - save(it, "recording") - } - this@Replay.info("Recording stopped. Recorded for ${recording?.duration}.") + stopRecording() } State.PLAYING, State.PAUSED_REPLAY, State.PLAYING_CHECKPOINTS -> { state = State.INACTIVE @@ -271,19 +286,26 @@ object Replay : Module( } } + private fun SafeContext.stopRecording() { + state = State.INACTIVE + recording?.let { + save(it, "recording") + this@Replay.info(buildText { + literal("Recording stopped. Recorded for ") + color(GuiSettings.primaryColor) { literal(it.duration.toString()) } + literal(".") + }) + } + } + private fun SafeContext.handleCheckpoint() { when (state) { State.RECORDING -> { - if (player.velocity != Vec3d(0.0, -0.0784000015258789, 0.0)) { - this@Replay.logError("Cannot create checkpoint while moving. Try again!") - return - } - checkpoint = recording?.duplicate() checkpoint?.let { save(it, "checkpoint") + this@Replay.info("Checkpoint created.") } - this@Replay.info("Checkpoint created.") } else -> {} } @@ -294,7 +316,10 @@ object Replay : Module( State.INACTIVE -> { state = State.PLAYING_CHECKPOINTS replay = checkpoint?.duplicate() - this@Replay.info("Replaying until last set checkpoint. Duration: ${checkpoint?.duration}") + info(buildText { + literal("Replaying until last set checkpoint. Duration: ") + color(GuiSettings.primaryColor) { literal(checkpoint?.duration.toString()) } + }) } else -> {} } @@ -302,14 +327,9 @@ object Replay : Module( private fun SafeContext.save(recording: Recording, name: String) { lambdaScope.launch(Dispatchers.IO) { - FolderRegister.replay.mkdirs() - FolderRegister.replay.resolve("$name-${ - mc.currentServerEntry?.address?.replace(":", "_") - }-${ - world.dimensionKey?.value?.path?.replace("/", "_") - }-${ - System.currentTimeMillis() - }.json").writeText(gsonCompact.toJson(recording)) + locationBoundDirectory(FolderRegister.replay).resolve("${ + Instant.now().toString().sanitizeForFilename() + }-$name.json").writeText(gsonCompact.toJson(recording)) } } diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index f91647cb5..5287b5ba9 100644 --- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt +++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt @@ -1,10 +1,13 @@ package com.lambda.util import com.lambda.Lambda.mc +import com.lambda.context.SafeContext import com.lambda.util.FolderRegister.config import com.lambda.util.FolderRegister.lambda import com.lambda.util.FolderRegister.minecraft +import com.lambda.util.StringUtils.sanitizeForFilename import java.io.File +import java.net.InetSocketAddress /** * The [FolderRegister] object is responsible for managing the directory structure of the application. @@ -19,4 +22,21 @@ object FolderRegister { val config: File = File(lambda, "config") val packetLogs: File = File(lambda, "packet-log") val replay: File = File(lambda, "replay") + + fun File.createIfNotExists() { + if (!exists()) { mkdirs() } + } + + fun File.listRecursive() = walk().filter { it.isFile } + + fun SafeContext.locationBoundDirectory(file: File): File { + val hostName = (connection.connection.address as? InetSocketAddress)?.hostName ?: "singleplayer" + val path = file.resolve( + hostName.sanitizeForFilename() + ).resolve( + world.dimensionKey?.value?.path?.sanitizeForFilename() ?: "unknown" + ) + path.createIfNotExists() + return path + } } diff --git a/common/src/main/kotlin/com/lambda/util/Formatting.kt b/common/src/main/kotlin/com/lambda/util/Formatting.kt index 768d6b821..e972bf4e3 100644 --- a/common/src/main/kotlin/com/lambda/util/Formatting.kt +++ b/common/src/main/kotlin/com/lambda/util/Formatting.kt @@ -8,7 +8,7 @@ import java.time.format.DateTimeFormatter object Formatting { val Vec3d.asString: String - get() = "(%.2f, %.2f, %.2f)".format(x, y, z) + get() = asString() fun Vec3d.asString(decimals: Int = 2): String { val format = "%.${decimals}f" diff --git a/common/src/main/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt index a0c9111bf..4ca5f7858 100644 --- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -1,6 +1,30 @@ package com.lambda.util object StringUtils { + fun String.sanitizeForFilename() = + replace("\\", "_") + .replace("/", "_") + .replace(":", "_") + .replace("*", "_") + .replace("?", "_") + .replace("\"", "_") + .replace("<", "_") + .replace(">", "_") + .replace("|", "_") + .trim() + .take(255) // truncate to 255 characters for Windows compatibility + + // ToDo: Fix this. Does not work for some reason +// fun String.sanitizeForFilename(): String { +// val invalidChars = Regex.fromLiteral("[\\\\/*?|<>:\"\\[\\]\\(\\)\\s]") +// val safeChars = Regex.fromLiteral("[^\\p{L}\\p{N}_\\-~]") +// return replace(invalidChars, "_") +// .replace(safeChars, "") +// .trim() +// .take(255) // truncate to 255 characters for Windows compatibility +// } + + fun String.capitalize() = replaceFirstChar { it.titlecase() } /** From 2ded48421d3201b31dd50e8eb0fb2c47275b8085 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 6 May 2024 05:35:00 +0200 Subject: [PATCH 13/14] Remove broken import --- .../src/main/kotlin/com/lambda/module/modules/player/Replay.kt | 1 - 1 file changed, 1 deletion(-) 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 fe4b40db1..0769febd8 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 @@ -5,7 +5,6 @@ import com.lambda.config.RotationSettings import com.lambda.context.SafeContext import com.lambda.core.TimerManager import com.lambda.event.EventFlow.lambdaScope -import com.lambda.event.events.InteractionEvent import com.lambda.event.events.KeyPressEvent import com.lambda.event.events.MovementEvent import com.lambda.event.events.RotationEvent From 4c784da6d2f90dbbe2fc701f5814bd6d24486dfb Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 6 May 2024 05:44:35 +0200 Subject: [PATCH 14/14] Remove todo --- .../src/main/kotlin/com/lambda/module/modules/player/Replay.kt | 1 - 1 file changed, 1 deletion(-) 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 0769febd8..87a099d5b 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 @@ -43,7 +43,6 @@ import kotlin.time.toDuration // ToDo: // - Record other types of inputs: (place, break, inventory, etc.) -// - Fancy logging // - Add HUD for recording / replaying info // - Maybe use a custom binary format to store the data (Protobuf / DB?) object Replay : Module(