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..d941ca030 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(entity.isSprinting())).getSprint(); + } + @Inject(method = "sendMovementPackets", at = @At(value = "HEAD"), cancellable = true) void sendBegin(CallbackInfo ci) { ci.cancel(); 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..6f22306fd --- /dev/null +++ b/common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt @@ -0,0 +1,49 @@ +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 +import com.lambda.util.FolderRegister.listRecursive + +object ReplayCommand : LambdaCommand { + override val name = "replay" + + init { + register(name, "rep") { + // 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 + required(string("replay name")) { replayName -> + suggests { _, builder -> + val dir = FolderRegister.replay + dir.listRecursive().forEach { + builder.suggest(it.relativeTo(dir).path) + } + builder.buildFuture() + } + + executeWithResult { + val replayFile = FolderRegister.replay.resolve(this[replayName].value()) + + 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/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..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( @@ -9,13 +11,13 @@ 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) + 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/event/events/MovementEvent.kt b/common/src/main/kotlin/com/lambda/event/events/MovementEvent.kt index d5036ed21..543074557 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(var sprint: Boolean) : MovementEvent() + class ClipAtLedge( var clip: Boolean, ) : MovementEvent() 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 new file mode 100644 index 000000000..87a099d5b --- /dev/null +++ b/common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -0,0 +1,449 @@ +package com.lambda.module.modules.player + +import com.google.gson.* +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 +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.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.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 +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.sound.SoundEvents +import net.minecraft.util.math.Vec3d +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 + +// ToDo: +// - Record other types of inputs: (place, break, inventory, etc.) +// - 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.", + defaultTags = setOf(ModuleTag.PLAYER, ModuleTag.AUTOMATION) +) { + 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("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) + private val deviationThreshold by setting("Deviation threshold", 0.1, 0.1..5.0, 0.1, description = "The threshold for the deviation to cancel the replay.") { cancelOnDeviation } + + private val rotationConfig = RotationSettings(this).apply { + rotationMode = RotationMode.LOCK + r1 = 1000.0 + r2 = 1001.0 + } + + enum class State { + INACTIVE, + RECORDING, + PAUSED_RECORDING, + PLAYING, + PAUSED_REPLAY, + PLAYING_CHECKPOINTS, + PAUSED_CHECKPOINTS + } + + private var state = State.INACTIVE + + private var checkpoint: Recording? = null + + 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()) + .create() + + fun loadRecording(file: File) { + recording = gsonCompact.fromJson(file.readText(), Recording::class.java) + + info(buildText { + literal("Recording ") + color(GuiSettings.primaryColor) { literal(file.nameWithoutExtension) } + literal(" loaded. Duration: ") + color(GuiSettings.primaryColor) { literal(recording?.duration.toString()) } + }) + } + + init { + listener { + if (mc.currentScreen != null && !mc.options.commandKey.isPressed) return@listener + + when (it.key) { + record.key -> handleRecord() + play.key -> handlePlay() + stop.key -> handleStop() + check.key -> handleCheckpoint() + playCheck.key -> handlePlayCheckpoints() + else -> {} + } + } + + listener { event -> + when (state) { + State.RECORDING -> { + recording?.let { + it.input.add(event.input.toAction()) + it.position.add(player.pos) + } + } + 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 + + 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.logError("Replay cancelled due to exceeding deviation threshold.") + return@listener + } + } + } + } + else -> {} + } + } + + listener { event -> + when (state) { + State.RECORDING -> { + recording?.rotation?.add(player.rotation) + } + State.PLAYING, State.PLAYING_CHECKPOINTS -> { + replay?.rotation?.removeFirstOrNull()?.let { rot -> + event.context = RotationContext(rot, rotationConfig) + } + } + else -> {} + } + } + + listener { event -> + when (state) { + State.RECORDING -> { + recording?.sprint?.add(player.isSprinting) + } + State.PLAYING, State.PLAYING_CHECKPOINTS -> { + replay?.sprint?.removeFirstOrNull()?.let { sprint -> + event.sprint = sprint + player.isSprinting = sprint + } + } + 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(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(GuiSettings.primaryColor) { literal(recording?.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...") + } + } + } + else -> {} + } + } + } + + private fun handlePlay() { + when (state) { + State.INACTIVE -> { + recording?.let { + state = State.PLAYING + replay = it.duplicate() + 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 + info("Recording paused.") + } + State.PAUSED_RECORDING -> { + state = State.RECORDING + info("Recording resumed.") + } + State.PLAYING -> { // ToDo: More general pausing for all states + state = State.PAUSED_REPLAY + info("Replay paused.") + } + State.PAUSED_REPLAY -> { + state = State.PLAYING + 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.") + } + } + } + + private fun SafeContext.handleRecord() { + when (state) { + State.RECORDING -> { + 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.") + } + else -> {} + } + } + + private fun SafeContext.handleStop() { + when (state) { + State.RECORDING, State.PAUSED_RECORDING -> { + stopRecording() + } + State.PLAYING, State.PAUSED_REPLAY, State.PLAYING_CHECKPOINTS -> { + state = State.INACTIVE + this@Replay.info("Replay stopped.") + } + else -> {} + } + } + + 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 -> { + checkpoint = recording?.duplicate() + checkpoint?.let { + save(it, "checkpoint") + this@Replay.info("Checkpoint created.") + } + } + else -> {} + } + } + + private fun handlePlayCheckpoints() { + when (state) { + State.INACTIVE -> { + state = State.PLAYING_CHECKPOINTS + replay = checkpoint?.duplicate() + info(buildText { + literal("Replaying until last set checkpoint. Duration: ") + color(GuiSettings.primaryColor) { literal(checkpoint?.duration.toString()) } + }) + } + else -> {} + } + } + + private fun SafeContext.save(recording: Recording, name: String) { + lambdaScope.launch(Dispatchers.IO) { + locationBoundDirectory(FolderRegister.replay).resolve("${ + Instant.now().toString().sanitizeForFilename() + }-$name.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 interaction: MutableList = mutableListOf() + ) : JsonSerializer, JsonDeserializer { + 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 duplicate() = Recording( + input.take(size).toMutableList(), + rotation.take(size).toMutableList(), + 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( + val movementSideways: Float, + val movementForward: Float, + val pressingForward: Boolean, + val pressingBack: Boolean, + val pressingLeft: Boolean, + val pressingRight: Boolean, + val jumping: Boolean, + 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 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 diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt index 493f26622..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. @@ -18,4 +21,22 @@ object FolderRegister { val lambda: File = File(minecraft, "lambda") 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 346c8d355..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,12 @@ 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" + return "(${format.format(x)}, ${format.format(y)}, ${format.format(z)})" + } fun getTime(formatter: DateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME): String { val localDateTime = LocalDateTime.now() 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() } /**