Skip to content

Commit 43a81f9

Browse files
committed
Save and load replays
1 parent c2cb2c5 commit 43a81f9

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

common/src/main/kotlin/com/lambda/command/commands/ReplayCommand.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package com.lambda.command.commands
22

3+
import com.lambda.brigadier.CommandResult
4+
import com.lambda.brigadier.argument.string
5+
import com.lambda.brigadier.argument.value
6+
import com.lambda.brigadier.executeWithResult
7+
import com.lambda.brigadier.get
8+
import com.lambda.brigadier.required
39
import com.lambda.command.CommandManager.register
410
import com.lambda.command.LambdaCommand
11+
import com.lambda.module.modules.player.Replay
12+
import com.lambda.util.FolderRegister
513

614
object ReplayCommand : LambdaCommand {
715
override val name = "replay"
@@ -15,6 +23,27 @@ object ReplayCommand : LambdaCommand {
1523
// 5. Pause replay
1624
// 6. Resume replay
1725
// 7. Set replay speed
26+
required(string("replay name")) { replayName ->
27+
suggests { _, builder ->
28+
FolderRegister.replays.listFiles()?.map {
29+
it.nameWithoutExtension
30+
}?.forEach {
31+
builder.suggest(it)
32+
}
33+
builder.buildFuture()
34+
}
35+
36+
executeWithResult {
37+
val replayFile = FolderRegister.replays.resolve("${this[replayName].value()}.json")
38+
39+
if (!replayFile.exists()) {
40+
return@executeWithResult CommandResult.failure("Replay file does not exist")
41+
}
42+
43+
Replay.loadRecording(replayFile)
44+
CommandResult.success()
45+
}
46+
}
1847
}
1948
}
2049
}

common/src/main/kotlin/com/lambda/module/modules/player/Replay.kt

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.lambda.module.modules.player
22

3+
import com.google.gson.*
34
import com.google.gson.annotations.SerializedName
45
import com.lambda.config.RotationSettings
56
import com.lambda.context.SafeContext
67
import com.lambda.core.TimerManager
8+
import com.lambda.event.EventFlow.lambdaScope
79
import com.lambda.event.events.KeyPressEvent
810
import com.lambda.event.events.MovementEvent
911
import com.lambda.event.events.RotationEvent
@@ -15,11 +17,17 @@ import com.lambda.module.Module
1517
import com.lambda.module.modules.player.Replay.InputAction.Companion.toAction
1618
import com.lambda.module.tag.ModuleTag
1719
import com.lambda.util.Communication.info
20+
import com.lambda.util.Communication.logError
1821
import com.lambda.util.Communication.warn
22+
import com.lambda.util.FolderRegister
1923
import com.lambda.util.KeyCode
2024
import com.lambda.util.primitives.extension.rotation
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.launch
2127
import net.minecraft.client.input.Input
2228
import net.minecraft.util.math.Vec3d
29+
import java.io.File
30+
import java.lang.reflect.Type
2331
import kotlin.time.Duration
2432
import kotlin.time.DurationUnit
2533
import kotlin.time.toDuration
@@ -29,7 +37,6 @@ import kotlin.time.toDuration
2937
// - Actually store the data in a file
3038
// - Implement a way to save and load the data (Commands?)
3139
// - Record other types of inputs: (Interactions, etc.)
32-
// - Fix continue deviation after replaying the checkpoint for spliced runs
3340
object Replay : Module(
3441
name = "Replay",
3542
description = "Record gameplay actions and replay them like a TAS.",
@@ -66,6 +73,16 @@ object Replay : Module(
6673
private var replay: Recording? = null
6774
private var repeats = 0
6875

76+
private val gsonCompact = GsonBuilder()
77+
.registerTypeAdapter(Recording::class.java, Recording())
78+
.create()
79+
80+
fun loadRecording(file: File) {
81+
recording = gsonCompact.fromJson(file.readText(), Recording::class.java)
82+
83+
info("Recording ${file.nameWithoutExtension} loaded. Duration: ${recording?.duration}.")
84+
}
85+
6986
init {
7087
listener<KeyPressEvent> {
7188
if (mc.currentScreen != null && !mc.options.commandKey.isPressed) return@listener
@@ -232,11 +249,21 @@ object Replay : Module(
232249
when (state) {
233250
State.RECORDING -> {
234251
if (player.velocity != Vec3d(0.0, -0.0784000015258789, 0.0)) {
235-
this@Replay.info("Cannot create checkpoint while moving.")
252+
this@Replay.logError("Cannot create checkpoint while moving. Try again!")
236253
return
237254
}
238255

239256
checkpoint = recording?.duplicate()
257+
lambdaScope.launch(Dispatchers.IO) {
258+
FolderRegister.replays.mkdirs()
259+
FolderRegister.replays.resolve("checkpoint-${
260+
mc.currentServerEntry?.address?.replace(":", "_")
261+
}-${
262+
world.dimensionKey?.value?.path?.replace("/", "_")
263+
}-${
264+
System.currentTimeMillis()
265+
}.json").writeText(gsonCompact.toJson(checkpoint))
266+
}
240267
this@Replay.info("Checkpoint created.")
241268
}
242269
else -> {}
@@ -259,7 +286,7 @@ object Replay : Module(
259286
val rotation: MutableList<Rotation> = mutableListOf(),
260287
val sprint: MutableList<Boolean> = mutableListOf(),
261288
val position: MutableList<Vec3d> = mutableListOf()
262-
) {
289+
) : JsonSerializer<Recording>, JsonDeserializer<Recording> {
263290
val size: Int
264291
get() = minOf(input.size, rotation.size, sprint.size, position.size)
265292
val duration: Duration
@@ -271,6 +298,66 @@ object Replay : Module(
271298
sprint.take(size).toMutableList(),
272299
position.take(size).toMutableList()
273300
)
301+
302+
override fun serialize(
303+
src: Recording?,
304+
typeOfSrc: Type?,
305+
context: JsonSerializationContext?,
306+
): JsonElement = src?.let { recording ->
307+
JsonArray().apply {
308+
repeat(recording.size) { i ->
309+
add(JsonArray().apply {
310+
val inputI = recording.input[i]
311+
add(inputI.movementSideways)
312+
add(inputI.movementForward)
313+
add(inputI.pressingForward)
314+
add(inputI.pressingBack)
315+
add(inputI.pressingLeft)
316+
add(inputI.pressingRight)
317+
add(inputI.jumping)
318+
add(inputI.sneaking)
319+
val rotationI = recording.rotation[i]
320+
add(rotationI.yaw)
321+
add(rotationI.pitch)
322+
add(recording.sprint[i])
323+
val positionI = recording.position[i]
324+
add(positionI.x)
325+
add(positionI.y)
326+
add(positionI.z)
327+
})
328+
}
329+
}
330+
} ?: JsonNull.INSTANCE
331+
332+
override fun deserialize(
333+
json: JsonElement?,
334+
typeOfT: Type?,
335+
context: JsonDeserializationContext?
336+
): Recording = json?.asJsonArray?.let {
337+
val input = mutableListOf<InputAction>()
338+
val rotation = mutableListOf<Rotation>()
339+
val sprint = mutableListOf<Boolean>()
340+
val position = mutableListOf<Vec3d>()
341+
342+
it.forEach { element ->
343+
val array = element.asJsonArray
344+
input.add(InputAction(
345+
array[0].asFloat,
346+
array[1].asFloat,
347+
array[2].asBoolean,
348+
array[3].asBoolean,
349+
array[4].asBoolean,
350+
array[5].asBoolean,
351+
array[6].asBoolean,
352+
array[7].asBoolean
353+
))
354+
rotation.add(Rotation(array[8].asDouble, array[9].asDouble))
355+
sprint.add(array[10].asBoolean)
356+
position.add(Vec3d(array[11].asDouble, array[12].asDouble, array[13].asDouble))
357+
}
358+
359+
Recording(input, rotation, sprint, position)
360+
} ?: Recording()
274361
}
275362

276363
data class InputAction(

common/src/main/kotlin/com/lambda/util/FolderRegister.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ object FolderRegister {
1818
val lambda: File = File(minecraft, "lambda")
1919
val config: File = File(lambda, "config")
2020
val packetLogs: File = File(lambda, "packet-log")
21+
val replays: File = File(lambda, "replays")
2122
}

0 commit comments

Comments
 (0)