From 83e477d32181917eb6aebb63c20d857c7da9b388 Mon Sep 17 00:00:00 2001 From: Scribble Date: Sun, 12 Apr 2026 17:29:36 +0200 Subject: [PATCH 1/4] [Playback] Fix tick 0 being null The 0th tick in the input list will now contain the state the keyboard was in when the recording was started - Fix issues when the VirtualPeripherals tried to copy from itself - Fix mixin.json being invalid - Add "tasmod.playback.trace" vm variable - Fix DesyncMonitor being 0 in the first tick --- .../com/minecrafttas/tasmod/TASmodClient.java | 3 +- .../playback/PlaybackControllerClient.java | 47 +++++++++---------- .../DesyncMonitorFileCommandExtension.java | 2 +- .../tasmod/registries/TASmodKeybinds.java | 1 + .../savestates/SavestateHandlerClient.java | 2 +- .../minecrafttas/tasmod/util/DebugWriter.java | 4 +- .../tasmod/virtual/VirtualCameraAngle.java | 4 +- .../tasmod/virtual/VirtualInput.java | 17 ++++++- .../tasmod/virtual/VirtualKeyboard.java | 6 +-- .../tasmod/virtual/VirtualMouse.java | 6 +-- src/main/resources/tasmod.mixin.json | 3 +- 11 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/minecrafttas/tasmod/TASmodClient.java b/src/main/java/com/minecrafttas/tasmod/TASmodClient.java index 8dc8ec4c..da2fbe86 100644 --- a/src/main/java/com/minecrafttas/tasmod/TASmodClient.java +++ b/src/main/java/com/minecrafttas/tasmod/TASmodClient.java @@ -99,7 +99,7 @@ public class TASmodClient implements ClientModInitializer, EventClientInit, Even * The container where all inputs get stored during recording or stored and * ready to be played back */ - public static PlaybackControllerClient controller = new PlaybackControllerClient(); + public static PlaybackControllerClient controller; public static void createTASfileDir() { try { @@ -131,6 +131,7 @@ public void onInitializeClient() { loadConfig(mc); virtual = new VirtualInput(LOGGER); + controller = new PlaybackControllerClient(virtual, LOGGER); // Initialize InfoHud hud = new InfoHud(); diff --git a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java index 6c48a0ba..16bb298e 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java @@ -34,7 +34,6 @@ import com.minecrafttas.mctcommon.networking.exception.WrongSideException; import com.minecrafttas.mctcommon.networking.interfaces.ClientPacketHandler; import com.minecrafttas.mctcommon.networking.interfaces.PacketID; -import com.minecrafttas.tasmod.TASmod; import com.minecrafttas.tasmod.TASmodClient; import com.minecrafttas.tasmod.events.EventClient.EventClientTickPost; import com.minecrafttas.tasmod.events.EventClient.EventDrawScreen; @@ -52,12 +51,12 @@ import com.minecrafttas.tasmod.playback.tasfile.exception.PlaybackSaveException; import com.minecrafttas.tasmod.registries.TASmodConfig; import com.minecrafttas.tasmod.registries.TASmodPackets; +import com.minecrafttas.tasmod.util.DebugWriter; import com.minecrafttas.tasmod.util.Ducks.GuiScreenDuck; import com.minecrafttas.tasmod.util.LoggerMarkers; import com.minecrafttas.tasmod.util.Scheduler.Task; import com.minecrafttas.tasmod.virtual.VirtualCameraAngle; import com.minecrafttas.tasmod.virtual.VirtualInput; -import com.minecrafttas.tasmod.virtual.VirtualInput.VirtualCameraAngleInput; import com.minecrafttas.tasmod.virtual.VirtualKeyboard; import com.minecrafttas.tasmod.virtual.VirtualMouse; @@ -102,7 +101,7 @@ public class PlaybackControllerClient implements //@formatter:on { - private Logger logger = TASmod.LOGGER; + private final Logger logger; /** * The current state of the controller. @@ -119,6 +118,11 @@ public class PlaybackControllerClient implements */ private long index; + /** + * The virtual input instance + */ + private final VirtualInput virtual; + /** *

The current keyboard used in the {@link PlaybackControllerClient PlaybackController} *

Used during recording to store incoming inputs from the {@link VirtualInput#KEYBOARD}
@@ -171,9 +175,10 @@ public class PlaybackControllerClient implements // ===================================================================================================== - public PlaybackControllerClient() { + public PlaybackControllerClient(VirtualInput virtual, Logger logger) { + this.virtual = virtual; + this.logger = logger; tasFileDirectory = TASmodClient.tasfiledirectory; - inputs = new BigArrayList(tasFileDirectory.resolve("temp").toAbsolutePath().toString()); } @@ -230,11 +235,9 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { switch (stateIn) { case PLAYBACK: startPlayback(); - state = TASstate.PLAYBACK; return verbose ? TextFormatting.GREEN + "Starting playback" : ""; case RECORDING: startRecording(); - state = TASstate.RECORDING; return verbose ? TextFormatting.GREEN + "Starting a recording" : ""; case PAUSED: return verbose ? TextFormatting.RED + "Can't pause anything because nothing is running" : ""; @@ -254,7 +257,6 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { return verbose ? TextFormatting.GREEN + "Pausing a recording" : ""; case NONE: stopRecording(); - state = TASstate.NONE; return verbose ? TextFormatting.GREEN + "Stopping the recording" : ""; } } else if (state == TASstate.PLAYBACK) { // If the container is currently playing back @@ -264,13 +266,12 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { case RECORDING: stopPlayback(false); startRecording(); - state = TASstate.RECORDING; return verbose ? TextFormatting.GREEN + "Switching from playback to recording" : ""; case PAUSED: LOGGER.debug(LoggerMarkers.Playback, "Pausing a playback"); state = TASstate.PAUSED; stateAfterPause = TASstate.PLAYBACK; - TASmodClient.virtual.clearNext(); + virtual.clearNext(); return verbose ? TextFormatting.GREEN + "Pausing a playback" : ""; case NONE: stopPlayback(true); @@ -294,9 +295,9 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { case NONE: LOGGER.debug(LoggerMarkers.Playback, "Aborting pausing"); state = TASstate.NONE; - TASstate statey = stateAfterPause; + TASstate stateAfterPauseTemp = stateAfterPause; stateAfterPause = TASstate.NONE; - return TextFormatting.GREEN + "Aborting a " + statey.toString().toLowerCase() + " that was paused"; + return TextFormatting.GREEN + "Aborting a " + stateAfterPauseTemp.toString().toLowerCase() + " that was paused"; } } return "Something went wrong ._."; @@ -304,34 +305,33 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { private void startRecording() { LOGGER.debug(LoggerMarkers.Playback, "Starting recording"); + state = TASstate.RECORDING; if (this.inputs.isEmpty()) { - VirtualCameraAngleInput CAMERA_ANGLE = TASmodClient.virtual.CAMERA_ANGLE; - Float pitch = CAMERA_ANGLE.getCurrentPitch(); - Float yaw = CAMERA_ANGLE.getCurrentYaw(); - this.currentPlaybackCameraAngle.set(pitch, yaw); - - inputs.add(new InputContainer()); + InputContainer preloadedContainer = virtual.preloadInputs(); + inputs.add(preloadedContainer); } } private void stopRecording() { LOGGER.debug(LoggerMarkers.Playback, "Stopping a recording"); - TASmodClient.virtual.clearNext(); + virtual.clearNext(); + state = TASstate.NONE; } private void startPlayback() { LOGGER.debug(LoggerMarkers.Playback, "Starting playback"); Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 index = 0; -// TASmod.ktrngHandler.setInitialSeed(startSeed); + state = TASstate.PLAYBACK; } private void stopPlayback(boolean clearInputs) { LOGGER.debug(LoggerMarkers.Playback, "Stopping a playback"); Minecraft.getMinecraft().gameSettings.chatLinks = true; if (clearInputs) { - TASmodClient.virtual.clearNext(); + virtual.clearNext(); } + state = TASstate.NONE; } /** @@ -479,9 +479,7 @@ public void onClientTickPost(Minecraft mc) { playbackNextTick(); } -// if (TASmod.isDevEnvironment) { -// DebugWriter.writeDebugFile(this); -// } + DebugWriter.writeDebugFile(this); } private void recordNextTick() { @@ -513,7 +511,6 @@ private void playbackNextTick() { /* Stop condition */ if (index == inputs.size() || inputs.isEmpty()) { - index--; unpressContainer(); setTASState(TASstate.NONE); } diff --git a/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java b/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java index 1a383e98..023e39a7 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java @@ -69,7 +69,7 @@ public String[] getFileCommandNames() { @Override public void onControllerStateChange(TASstate newstate, TASstate oldstate) { if (newstate == TASstate.RECORDING && monitorContainer.isEmpty()) { - recordNull(0); + onRecord(0, null); } currentValues = null; } diff --git a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java index bbd88526..1d2db074 100644 --- a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java +++ b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java @@ -46,6 +46,7 @@ public enum TASmodKeybinds implements KeybindID { TASmodClient.virtual.CAMERA_ANGLE.updateNextCameraAngle(0, 45); }), TEST1("Various Testing", "TASmod", Keyboard.KEY_F12, () -> { + TASmodClient.controller.setTASState(TASstate.RECORDING); }, VirtualKeybindings::isKeyDown), TEST2("Various Testing2", "TASmod", Keyboard.KEY_F7, () -> { }, VirtualKeybindings::isKeyDown); diff --git a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java index 24e23965..adb2d222 100644 --- a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java +++ b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java @@ -269,7 +269,7 @@ else if (state == TASstate.PLAYBACK) { private static void preload(BigArrayList containerList, long index) { LOGGER.trace(LoggerMarkers.Savestate, "Preloading container at index {}", index); InputContainer containerToPreload = containerList.get(index); - TASmodClient.virtual.preloadInput(containerToPreload.getKeyboard(), containerToPreload.getMouse(), containerToPreload.getCameraAngle()); + TASmodClient.virtual.preloadInputs(containerToPreload.getKeyboard(), containerToPreload.getMouse(), containerToPreload.getCameraAngle()); TASmodAPIRegistry.PLAYBACK_FILE_COMMAND.onPlaybackTick(index, containerToPreload); } diff --git a/src/main/java/com/minecrafttas/tasmod/util/DebugWriter.java b/src/main/java/com/minecrafttas/tasmod/util/DebugWriter.java index 58988346..83a3c674 100644 --- a/src/main/java/com/minecrafttas/tasmod/util/DebugWriter.java +++ b/src/main/java/com/minecrafttas/tasmod/util/DebugWriter.java @@ -16,6 +16,8 @@ public class DebugWriter { private static Path debugTASFile = TASmodClient.tasfiledirectory.resolve("debug.mctas"); public static void writeDebugFile(PlaybackControllerClient controller) { - PlaybackSerialiser.saveToFile(debugTASFile, controller, null); + if (System.getProperty("tasmod.playback.trace", "false").equals("true")) { + PlaybackSerialiser.saveToFile(debugTASFile, controller, null); + } } } diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualCameraAngle.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualCameraAngle.java index 5dc7264c..50a00047 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualCameraAngle.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualCameraAngle.java @@ -142,7 +142,7 @@ public void getStates(List reference) { * @param camera The camera to copy from */ public void moveFrom(VirtualCameraAngle camera) { - if (camera == null) + if (camera == null || this == camera) return; this.pitch = camera.pitch; this.yaw = camera.yaw; @@ -156,7 +156,7 @@ public void moveFrom(VirtualCameraAngle camera) { * @param camera The camera to copy from */ public void deepCopyFrom(VirtualCameraAngle camera) { - if (camera == null || !camera.isParent()) + if (camera == null || this == camera || !camera.isParent()) return; this.pitch = camera.pitch; this.yaw = camera.yaw; diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java index 00753400..fcebabba 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java @@ -13,6 +13,7 @@ import com.minecrafttas.tasmod.events.EventVirtualInput; import com.minecrafttas.tasmod.mixin.playbackhooks.MixinEntityRenderer; import com.minecrafttas.tasmod.mixin.playbackhooks.MixinMinecraft; +import com.minecrafttas.tasmod.playback.PlaybackControllerClient.InputContainer; import com.minecrafttas.tasmod.util.Ducks; import com.minecrafttas.tasmod.util.Ducks.SubtickDuck; import com.minecrafttas.tasmod.util.LoggerMarkers; @@ -198,7 +199,21 @@ public void clearCurrent() { MOUSE.clearCurrent(); } - public void preloadInput(VirtualKeyboard keyboardToPreload, VirtualMouse mouseToPreload, VirtualCameraAngle angleToPreload) { + /** + * Preloads the nextInputs into the currentInputs + */ + public InputContainer preloadInputs() { + preloadInputs(KEYBOARD.nextKeyboard, MOUSE.nextMouse, CAMERA_ANGLE.nextCameraAngle); + return new InputContainer(KEYBOARD.nextKeyboard.clone(), MOUSE.nextMouse.clone(), CAMERA_ANGLE.nextCameraAngle.clone()); + } + + /** + * Preloads the next and current inputs with the Virtual Keyboard mouse and camera angle + * @param keyboardToPreload + * @param mouseToPreload + * @param angleToPreload + */ + public void preloadInputs(VirtualKeyboard keyboardToPreload, VirtualMouse mouseToPreload, VirtualCameraAngle angleToPreload) { // Preload the nextKeyboard KEYBOARD.nextKeyboard.deepCopyFrom(keyboardToPreload); MOUSE.nextMouse.deepCopyFrom(mouseToPreload); diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualKeyboard.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualKeyboard.java index b35260cc..00d71a96 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualKeyboard.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualKeyboard.java @@ -336,7 +336,7 @@ public VirtualKeyboard clone() { @Override public void moveFrom(VirtualKeyboard keyboard) { - if (keyboard == null) + if (keyboard == null || this == keyboard) return; super.moveFrom(keyboard); charList.clear(); @@ -346,7 +346,7 @@ public void moveFrom(VirtualKeyboard keyboard) { @Override public void copyFrom(VirtualKeyboard keyboard) { - if (keyboard == null) + if (keyboard == null || this == keyboard) return; super.copyFrom(keyboard); charList.clear(); @@ -355,7 +355,7 @@ public void copyFrom(VirtualKeyboard keyboard) { @Override public void deepCopyFrom(VirtualKeyboard keyboard) { - if (keyboard == null) + if (keyboard == null || this == keyboard) return; super.deepCopyFrom(keyboard); charList.clear(); diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualMouse.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualMouse.java index 1a475b77..71d25919 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualMouse.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualMouse.java @@ -295,7 +295,7 @@ public VirtualMouse clone() { @Override public void moveFrom(VirtualMouse mouse) { - if (mouse == null) + if (mouse == null || this == mouse) return; super.moveFrom(mouse); this.scrollWheel = mouse.scrollWheel; @@ -306,7 +306,7 @@ public void moveFrom(VirtualMouse mouse) { @Override public void copyFrom(VirtualMouse mouse) { - if (mouse == null) + if (mouse == null || this == mouse) return; super.copyFrom(mouse); this.scrollWheel = mouse.scrollWheel; @@ -316,7 +316,7 @@ public void copyFrom(VirtualMouse mouse) { @Override public void deepCopyFrom(VirtualMouse mouse) { - if (mouse == null) + if (mouse == null || this == mouse) return; super.deepCopyFrom(mouse); this.scrollWheel = mouse.scrollWheel; diff --git a/src/main/resources/tasmod.mixin.json b/src/main/resources/tasmod.mixin.json index 5d2c5e53..1c5e6b97 100644 --- a/src/main/resources/tasmod.mixin.json +++ b/src/main/resources/tasmod.mixin.json @@ -30,7 +30,6 @@ "killtherng.mathrand.MixinEntityXPOrb", "killtherng.mathrand.MixinEntityTNTPrimed", "killtherng.mathrand.MixinWorldEntitySpawner" - ], "client": [ // General @@ -79,7 +78,7 @@ "fixes.MixinMouseHelper", // KTRNG - "killtherng.MixinRenderLivingBase" + "killtherng.MixinRenderLivingBase", // Tickrate Rendering "tickrate.MixinAudioPitch", From 83fd7f1f6e244c2cee38c0f4c34b8f6ed5327988 Mon Sep 17 00:00:00 2001 From: Scribble Date: Sun, 12 Apr 2026 21:27:35 +0200 Subject: [PATCH 2/4] [Playback] Preload inputs on playback --- .../playback/PlaybackControllerClient.java | 58 +++++++++++-------- .../playback/PlaybackControllerServer.java | 22 +++++-- .../tasmod/registries/TASmodKeybinds.java | 8 ++- .../savestates/SavestateHandlerClient.java | 2 +- .../tasmod/virtual/VirtualInput.java | 4 ++ 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java index 16bb298e..35b0c47d 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java @@ -320,9 +320,16 @@ private void stopRecording() { private void startPlayback() { LOGGER.debug(LoggerMarkers.Playback, "Starting playback"); + state = TASstate.PLAYBACK; + + InputContainer initialContainer = inputs.get(0); + this.nextPlaybackKeyboard = initialContainer.getKeyboard().clone(); + this.nextPlaybackMouse = initialContainer.getMouse().clone(); + this.nextPlaybackCameraAngle = initialContainer.getCameraAngle().clone(); + virtual.preloadInputs(initialContainer); + Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 index = 0; - state = TASstate.PLAYBACK; } private void stopPlayback(boolean clearInputs) { @@ -503,39 +510,40 @@ private void playbackNextTick() { // secondly as potential exploit protection LOGGER.info(LoggerMarkers.Playback, "Stopping a {} since the user tabbed out of the game", state); setTASState(TASstate.NONE); + return; } - index++; // Increase the index and load the next inputs - - EventListenerRegistry.fireEvent(EventPlaybackTickPre.class, index); - /* Stop condition */ - if (index == inputs.size() || inputs.isEmpty()) { + if (index + 1 == inputs.size() || inputs.isEmpty()) { unpressContainer(); setTASState(TASstate.NONE); + return; } + + index++; // Increase the index and load the next inputs + + EventListenerRegistry.fireEvent(EventPlaybackTickPre.class, index); + /* Continue condition */ - else { - InputContainer container = null; - if (index + 1 < inputs.size()) { - container = inputs.get(index + 1); // Loads the new inputs from the container - - this.currentPlaybackKeyboard = this.nextPlaybackKeyboard.clone(); - this.currentPlaybackMouse = this.nextPlaybackMouse.clone(); - this.currentPlaybackCameraAngle = this.nextPlaybackCameraAngle.clone(); - - this.nextPlaybackKeyboard = container.getKeyboard().clone(); - this.nextPlaybackMouse = container.getMouse().clone(); - this.nextPlaybackCameraAngle = container.getCameraAngle().clone(); - } else { - container = inputs.get(index); // Loads the new inputs from the container - this.currentPlaybackKeyboard = container.getKeyboard().clone(); - this.currentPlaybackMouse = container.getMouse().clone(); - this.currentPlaybackCameraAngle = container.getCameraAngle().clone(); - } + InputContainer container = null; + if (index + 1 < inputs.size()) { + container = inputs.get(index + 1); // Loads the new inputs from the container + + this.currentPlaybackKeyboard = this.nextPlaybackKeyboard.clone(); + this.currentPlaybackMouse = this.nextPlaybackMouse.clone(); + this.currentPlaybackCameraAngle = this.nextPlaybackCameraAngle.clone(); - EventListenerRegistry.fireEvent(EventPlaybackTick.class, index, container); + this.nextPlaybackKeyboard = container.getKeyboard().clone(); + this.nextPlaybackMouse = container.getMouse().clone(); + this.nextPlaybackCameraAngle = container.getCameraAngle().clone(); + } else { + container = inputs.get(index); // Loads the new inputs from the container + this.currentPlaybackKeyboard = container.getKeyboard().clone(); + this.currentPlaybackMouse = container.getMouse().clone(); + this.currentPlaybackCameraAngle = container.getCameraAngle().clone(); } + + EventListenerRegistry.fireEvent(EventPlaybackTick.class, index, container); } // ===================================================================================================== // Methods to manipulate inputs diff --git a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerServer.java b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerServer.java index 808d86ff..ef697ee7 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerServer.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerServer.java @@ -12,6 +12,7 @@ import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_RESTARTANDPLAY; import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_SAVE; import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_STATE; +import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_STATE_TEMP_SAVESTATE; import static com.minecrafttas.tasmod.registries.TASmodPackets.SAVESTATE_CLEAR_SCREEN; import static com.minecrafttas.tasmod.util.LoggerMarkers.Playback; @@ -50,6 +51,7 @@ public PacketID[] getAcceptedPacketIDs() { return new TASmodPackets[] { PLAYBACK_STATE, + PLAYBACK_STATE_TEMP_SAVESTATE, PLAYBACK_CLEAR_INPUTS, PLAYBACK_FULLPLAY, PLAYBACK_FULLRECORD, @@ -73,21 +75,31 @@ public void onServerPacket(PacketID id, ByteBuffer buf, String username) throws TASstate networkState = TASmodBufferBuilder.readEnum(TASstate.class, buf); /* TODO Permissions */ - setTASState(networkState); + TASmod.gameLoopSchedulerServer.add(() -> { + setTASState(networkState); + }); break; case PLAYBACK_CLEAR_INPUTS: - clearInputs(); + TASmod.gameLoopSchedulerServer.add(() -> { + clearInputs(); + }); break; case PLAYBACK_FULLRECORD: - fullRecord(); + TASmod.gameLoopSchedulerServer.add(() -> { + fullRecord(); + }); break; case PLAYBACK_FULLPLAY: - fullPlay(); + TASmod.gameLoopSchedulerServer.add(() -> { + fullPlay(); + }); break; case PLAYBACK_RESTARTANDPLAY: String tasFileName = TASmodBufferBuilder.readString(buf); - restartAndPlay(tasFileName); + TASmod.gameLoopSchedulerServer.add(() -> { + restartAndPlay(tasFileName); + }); break; case PLAYBACK_SAVE: case PLAYBACK_LOAD: diff --git a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java index 1d2db074..4473c8e0 100644 --- a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java +++ b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java @@ -1,5 +1,7 @@ package com.minecrafttas.tasmod.registries; +import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_STATE_TEMP_SAVESTATE; + import org.lwjgl.input.Keyboard; import com.minecrafttas.mctcommon.KeybindManager.IsKeyDownFunc; @@ -46,7 +48,11 @@ public enum TASmodKeybinds implements KeybindID { TASmodClient.virtual.CAMERA_ANGLE.updateNextCameraAngle(0, 45); }), TEST1("Various Testing", "TASmod", Keyboard.KEY_F12, () -> { - TASmodClient.controller.setTASState(TASstate.RECORDING); + try { + TASmodClient.client.send(new TASmodBufferBuilder(PLAYBACK_STATE_TEMP_SAVESTATE).writeEnum(TASstate.RECORDING)); + } catch (Exception e) { + e.printStackTrace(); + } }, VirtualKeybindings::isKeyDown), TEST2("Various Testing2", "TASmod", Keyboard.KEY_F7, () -> { }, VirtualKeybindings::isKeyDown); diff --git a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java index adb2d222..64019477 100644 --- a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java +++ b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerClient.java @@ -269,7 +269,7 @@ else if (state == TASstate.PLAYBACK) { private static void preload(BigArrayList containerList, long index) { LOGGER.trace(LoggerMarkers.Savestate, "Preloading container at index {}", index); InputContainer containerToPreload = containerList.get(index); - TASmodClient.virtual.preloadInputs(containerToPreload.getKeyboard(), containerToPreload.getMouse(), containerToPreload.getCameraAngle()); + TASmodClient.virtual.preloadInputs(containerToPreload); TASmodAPIRegistry.PLAYBACK_FILE_COMMAND.onPlaybackTick(index, containerToPreload); } diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java index fcebabba..b4283502 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java @@ -207,6 +207,10 @@ public InputContainer preloadInputs() { return new InputContainer(KEYBOARD.nextKeyboard.clone(), MOUSE.nextMouse.clone(), CAMERA_ANGLE.nextCameraAngle.clone()); } + public void preloadInputs(InputContainer inputContainer) { + preloadInputs(inputContainer.getKeyboard(), inputContainer.getMouse(), inputContainer.getCameraAngle()); + } + /** * Preloads the next and current inputs with the Virtual Keyboard mouse and camera angle * @param keyboardToPreload From e0b02e12db3b2daf52af7d5c8d74d7d9b04aef5e Mon Sep 17 00:00:00 2001 From: Scribble Date: Tue, 14 Apr 2026 21:11:14 +0200 Subject: [PATCH 3/4] [Playback] Fix timing issues between recording and playback This commit makes it so the recording and playback will line up properly. Before this, the playback was delayed by one tick. To achieve this, the playbackNextTick and recordNextTick functions were split into different tick functions. Before, both methods were called in onClientTickPost, which is fired when the tick ends. This is correct for recordNextTick, however the playbackNextTick needs to be in a onClientTickPre method. - Added UnitTests for the PlaybackControllerClient - Fixed mistake where nextPlaybackKeyboard were used instead of currentPlaybackKeyboard - Replaced static TASmod Logger with local logger - Added onClientTickPre Event --- .../com/minecrafttas/tasmod/TASmodClient.java | 2 +- .../tasmod/events/EventClient.java | 12 + .../mixin/playbackhooks/MixinMinecraft.java | 3 + .../playback/PlaybackControllerClient.java | 146 ++++---- .../DesyncMonitorFileCommandExtension.java | 2 +- .../tasmod/virtual/VirtualInput.java | 18 +- src/test/java/tasmod/TestUtil.java | 8 + .../PlaybackControllerClientTest.java | 311 ++++++++++++++++++ 8 files changed, 427 insertions(+), 75 deletions(-) create mode 100644 src/test/java/tasmod/TestUtil.java create mode 100644 src/test/java/tasmod/playback/PlaybackControllerClientTest.java diff --git a/src/main/java/com/minecrafttas/tasmod/TASmodClient.java b/src/main/java/com/minecrafttas/tasmod/TASmodClient.java index da2fbe86..177b830f 100644 --- a/src/main/java/com/minecrafttas/tasmod/TASmodClient.java +++ b/src/main/java/com/minecrafttas/tasmod/TASmodClient.java @@ -131,7 +131,7 @@ public void onInitializeClient() { loadConfig(mc); virtual = new VirtualInput(LOGGER); - controller = new PlaybackControllerClient(virtual, LOGGER); + controller = new PlaybackControllerClient(virtual, tasfiledirectory, LOGGER); // Initialize InfoHud hud = new InfoHud(); diff --git a/src/main/java/com/minecrafttas/tasmod/events/EventClient.java b/src/main/java/com/minecrafttas/tasmod/events/EventClient.java index 7c4a4a6e..449d6ba2 100644 --- a/src/main/java/com/minecrafttas/tasmod/events/EventClient.java +++ b/src/main/java/com/minecrafttas/tasmod/events/EventClient.java @@ -45,6 +45,18 @@ public static interface EventDrawHotbarAlways extends EventBase { public void onDrawHotbarAlways(); } + /** + * Fired at the beginning of a client tick + */ + @FunctionalInterface + public static interface EventClientTickPre extends EventBase { + + /** + * Fired at the beginning of a client tick + */ + public void onClientTickPre(Minecraft mc); + } + /** * Fired at the end of a client tick */ diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java b/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java index 35bad375..8a0a87e5 100644 --- a/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java +++ b/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java @@ -9,7 +9,9 @@ import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import com.minecrafttas.mctcommon.events.EventListenerRegistry; import com.minecrafttas.tasmod.TASmodClient; +import com.minecrafttas.tasmod.events.EventClient; import com.minecrafttas.tasmod.virtual.SubtickGuiScreen; import com.minecrafttas.tasmod.virtual.VirtualInput; import com.minecrafttas.tasmod.virtual.VirtualInput.VirtualKeyboardInput; @@ -48,6 +50,7 @@ public void playback_injectRunGameLoop(CallbackInfo ci) { */ @Inject(method = "runTick", at = @At(value = "HEAD")) public void playback_injectRunTick(CallbackInfo ci) { + EventListenerRegistry.fireEvent(EventClient.EventClientTickPre.class, (Minecraft) (Object) this); // Fire this *before* nextKeyboardTick and nextMouseTick, or playing back ticks will desync! /* * Both of these were previously in injectRunTickKeyboard/Mouse * but moved to the beginning of runTick to fix #224 diff --git a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java index 35b0c47d..564dde01 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java @@ -1,6 +1,5 @@ package com.minecrafttas.tasmod.playback; -import static com.minecrafttas.tasmod.TASmod.LOGGER; import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_CLEAR_INPUTS; import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_FULLPLAY; import static com.minecrafttas.tasmod.registries.TASmodPackets.PLAYBACK_FULLRECORD; @@ -36,6 +35,7 @@ import com.minecrafttas.mctcommon.networking.interfaces.PacketID; import com.minecrafttas.tasmod.TASmodClient; import com.minecrafttas.tasmod.events.EventClient.EventClientTickPost; +import com.minecrafttas.tasmod.events.EventClient.EventClientTickPre; import com.minecrafttas.tasmod.events.EventClient.EventDrawScreen; import com.minecrafttas.tasmod.events.EventPlaybackClient; import com.minecrafttas.tasmod.events.EventPlaybackClient.EventControllerStateChange; @@ -92,6 +92,7 @@ public class PlaybackControllerClient implements ClientPacketHandler, EventClientInit, + EventClientTickPre, EventClientTickPost, EventDrawScreen, @@ -175,10 +176,10 @@ public class PlaybackControllerClient implements // ===================================================================================================== - public PlaybackControllerClient(VirtualInput virtual, Logger logger) { + public PlaybackControllerClient(VirtualInput virtual, Path tasFileDirectory, Logger logger) { this.virtual = virtual; this.logger = logger; - tasFileDirectory = TASmodClient.tasfiledirectory; + this.tasFileDirectory = tasFileDirectory; inputs = new BigArrayList(tasFileDirectory.resolve("temp").toAbsolutePath().toString()); } @@ -251,7 +252,7 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { case RECORDING: return TextFormatting.RED + "Please report this message to the mod author, because you should never be able to see this (Error: Recording)"; case PAUSED: - LOGGER.debug(LoggerMarkers.Playback, "Pausing a recording"); + logger.debug(LoggerMarkers.Playback, "Pausing a recording"); state = TASstate.PAUSED; stateAfterPause = TASstate.RECORDING; return verbose ? TextFormatting.GREEN + "Pausing a recording" : ""; @@ -268,7 +269,7 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { startRecording(); return verbose ? TextFormatting.GREEN + "Switching from playback to recording" : ""; case PAUSED: - LOGGER.debug(LoggerMarkers.Playback, "Pausing a playback"); + logger.debug(LoggerMarkers.Playback, "Pausing a playback"); state = TASstate.PAUSED; stateAfterPause = TASstate.PLAYBACK; virtual.clearNext(); @@ -281,19 +282,19 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { } else if (state == TASstate.PAUSED) { switch (stateIn) { case PLAYBACK: - LOGGER.debug(LoggerMarkers.Playback, "Resuming a playback"); + logger.debug(LoggerMarkers.Playback, "Resuming a playback"); state = TASstate.PLAYBACK; stateAfterPause = TASstate.NONE; return verbose ? TextFormatting.GREEN + "Resuming a playback" : ""; case RECORDING: - LOGGER.debug(LoggerMarkers.Playback, "Resuming a recording"); + logger.debug(LoggerMarkers.Playback, "Resuming a recording"); state = TASstate.RECORDING; stateAfterPause = TASstate.NONE; return verbose ? TextFormatting.GREEN + "Resuming a recording" : ""; case PAUSED: return TextFormatting.RED + "Please report this message to the mod author, because you should never be able to see this (Error: Paused)"; case NONE: - LOGGER.debug(LoggerMarkers.Playback, "Aborting pausing"); + logger.debug(LoggerMarkers.Playback, "Aborting pausing"); state = TASstate.NONE; TASstate stateAfterPauseTemp = stateAfterPause; stateAfterPause = TASstate.NONE; @@ -304,37 +305,39 @@ public String setTASStateClient(TASstate stateIn, boolean verbose) { } private void startRecording() { - LOGGER.debug(LoggerMarkers.Playback, "Starting recording"); + logger.debug(LoggerMarkers.Playback, "Starting recording"); state = TASstate.RECORDING; if (this.inputs.isEmpty()) { - InputContainer preloadedContainer = virtual.preloadInputs(); - inputs.add(preloadedContainer); + virtual.preloadInputs(); + inputs.add(new InputContainer(this.currentPlaybackKeyboard.clone(), this.currentPlaybackMouse.clone(), this.currentPlaybackCameraAngle.clone())); } } private void stopRecording() { - LOGGER.debug(LoggerMarkers.Playback, "Stopping a recording"); + logger.debug(LoggerMarkers.Playback, "Stopping a recording"); virtual.clearNext(); state = TASstate.NONE; } private void startPlayback() { - LOGGER.debug(LoggerMarkers.Playback, "Starting playback"); + logger.debug(LoggerMarkers.Playback, "Starting playback"); state = TASstate.PLAYBACK; InputContainer initialContainer = inputs.get(0); - this.nextPlaybackKeyboard = initialContainer.getKeyboard().clone(); - this.nextPlaybackMouse = initialContainer.getMouse().clone(); - this.nextPlaybackCameraAngle = initialContainer.getCameraAngle().clone(); + this.currentPlaybackKeyboard = initialContainer.getKeyboard().clone(); + this.currentPlaybackMouse = initialContainer.getMouse().clone(); + this.currentPlaybackCameraAngle = initialContainer.getCameraAngle().clone(); virtual.preloadInputs(initialContainer); - Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 + if (Minecraft.getMinecraft() != null) + Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 index = 0; } private void stopPlayback(boolean clearInputs) { - LOGGER.debug(LoggerMarkers.Playback, "Stopping a playback"); - Minecraft.getMinecraft().gameSettings.chatLinks = true; + logger.debug(LoggerMarkers.Playback, "Stopping a playback"); + if (Minecraft.getMinecraft() != null) + Minecraft.getMinecraft().gameSettings.chatLinks = true; if (clearInputs) { virtual.clearNext(); } @@ -361,7 +364,7 @@ public TASstate togglePause() { * @param pause True, if it should be paused */ public void pause(boolean pause) { - LOGGER.trace(LoggerMarkers.Playback, "Pausing {}", pause); + logger.trace(LoggerMarkers.Playback, "Pausing {}", pause); if (pause) { if (state != TASstate.NONE) { setTASStateClient(TASstate.PAUSED, false); @@ -452,49 +455,46 @@ public void onDrawScreen(GuiScreen screen, int x, int y) { Mouse.setCursorPosition(duckedScreen.rescaleX(x), duckedScreen.rescaleY(y)); } - /** - * Updates the input container.
- *
- * During a recording this adds the {@linkplain #currentPlaybackKeyboard}, {@linkplain #currentPlaybackMouse} - * and {@linkplain #currentPlaybackCameraAngle} to {@linkplain #inputs} and increases the - * {@linkplain #index}.
- *
- * During playback the opposite is happening, getting the inputs from - * {@linkplain #inputs} and temporarily storing them in {@linkplain #currentPlaybackKeyboard}, - * {@linkplain #currentPlaybackMouse} and {@linkplain #currentPlaybackCameraAngle}.
- *
- * Then in {@linkplain VirtualInput}, {@linkplain #currentPlaybackKeyboard}, - * {@linkplain #currentPlaybackMouse} and {@linkplain #currentPlaybackCameraAngle} are retrieved and emulated as - * the next inputs - */ + @Override + public void onClientTickPre(Minecraft mc) { + if (state == TASstate.PLAYBACK) { + /* Tick the next playback*/ + playbackNextTick(); + } + } + @Override public void onClientTickPost(Minecraft mc) { /* Stop the playback while player is still loading */ - EntityPlayerSP player = mc.player; - if (player != null && player.addedToChunk) { - if (isPaused() && stateAfterPause != TASstate.NONE) { // TODO Find a better solution... - setTASState(stateAfterPause); // The recording is paused in LoadWorldEvents#startLaunchServer - pause(false); - EventListenerRegistry.fireEvent(EventPlaybackJoinedWorld.class, state); + if (mc != null) { // Mc can be null during Unit Tests + EntityPlayerSP player = mc.player; + if (player != null && player.addedToChunk) { + if (isPaused() && stateAfterPause != TASstate.NONE) { // TODO Find a better solution... + setTASState(stateAfterPause); // The recording is paused in LoadWorldEvents#startLaunchServer + pause(false); + EventListenerRegistry.fireEvent(EventPlaybackJoinedWorld.class, state); + } } } - /* Tick the next playback or recording */ + /* Tick the next recording */ if (state == TASstate.RECORDING) { recordNextTick(); - } else if (state == TASstate.PLAYBACK) { - playbackNextTick(); } - DebugWriter.writeDebugFile(this); + if (mc != null) + DebugWriter.writeDebugFile(this); } + /** + * Records the inputs from {@link #currentPlaybackKeyboard} etc. into {@link #inputs} + */ private void recordNextTick() { index++; InputContainer container = new InputContainer(currentPlaybackKeyboard.clone(), currentPlaybackMouse.clone(), currentPlaybackCameraAngle.clone()); if (inputs.size() <= index) { if (inputs.size() < index) { - LOGGER.warn("Index is {} inputs bigger than the container!", index - inputs.size()); + logger.warn("Index is {} inputs bigger than the container!", index - inputs.size()); } inputs.add(container); } else { @@ -504,11 +504,15 @@ private void recordNextTick() { EventListenerRegistry.fireEvent(EventRecordTick.class, index, container); } + /** + * Retrieves the inputs from {@link #inputs} and adds them to {@link #currentPlaybackKeyboard} etc. + * Also updates {@link #nextPlaybackCameraAngle} which is only used for visualizing what the input in the next tick is + */ private void playbackNextTick() { Minecraft mc = Minecraft.getMinecraft(); - if (!Display.isActive() && mc.gameSettings.pauseOnLostFocus) { // Stops the playback when you tab out of minecraft, for once as a failsafe, - // secondly as potential exploit protection - LOGGER.info(LoggerMarkers.Playback, "Stopping a {} since the user tabbed out of the game", state); + if (mc != null && !Display.isActive() && mc.gameSettings.pauseOnLostFocus) { // Stops the playback when you tab out of minecraft, for once as a failsafe, + // secondly as potential exploit protection + logger.info(LoggerMarkers.Playback, "Stopping a {} since the user tabbed out of the game", state); setTASState(TASstate.NONE); return; } @@ -525,24 +529,24 @@ private void playbackNextTick() { EventListenerRegistry.fireEvent(EventPlaybackTickPre.class, index); /* Continue condition */ - InputContainer container = null; - if (index + 1 < inputs.size()) { - container = inputs.get(index + 1); // Loads the new inputs from the container - this.currentPlaybackKeyboard = this.nextPlaybackKeyboard.clone(); - this.currentPlaybackMouse = this.nextPlaybackMouse.clone(); - this.currentPlaybackCameraAngle = this.nextPlaybackCameraAngle.clone(); + if (index + 1 < inputs.size()) { + InputContainer nextContainer = inputs.get(index + 1); - this.nextPlaybackKeyboard = container.getKeyboard().clone(); - this.nextPlaybackMouse = container.getMouse().clone(); - this.nextPlaybackCameraAngle = container.getCameraAngle().clone(); + this.nextPlaybackKeyboard = nextContainer.getKeyboard().clone(); + this.nextPlaybackMouse = nextContainer.getMouse().clone(); + this.nextPlaybackCameraAngle = nextContainer.getCameraAngle().clone(); } else { - container = inputs.get(index); // Loads the new inputs from the container - this.currentPlaybackKeyboard = container.getKeyboard().clone(); - this.currentPlaybackMouse = container.getMouse().clone(); - this.currentPlaybackCameraAngle = container.getCameraAngle().clone(); + nextPlaybackKeyboard.clear(); + nextPlaybackMouse.clear(); + nextPlaybackCameraAngle.clear(); } + InputContainer container = inputs.get(index); // Loads the new inputs from the container + this.currentPlaybackKeyboard = container.getKeyboard().clone(); + this.currentPlaybackMouse = container.getMouse().clone(); + this.currentPlaybackCameraAngle = container.getCameraAngle().clone(); + EventListenerRegistry.fireEvent(EventPlaybackTick.class, index, container); } // ===================================================================================================== @@ -611,7 +615,7 @@ public InputContainer get() { } public void clear() { - LOGGER.info(LoggerMarkers.Playback, "Clearing playback controller"); + logger.info(LoggerMarkers.Playback, "Clearing playback controller"); clearInputList(); EventListenerRegistry.fireEvent(EventPlaybackClient.EventRecordClear.class); @@ -668,7 +672,7 @@ public String toString() { * Clears {@link #currentPlaybackKeyboard} and {@link #currentPlaybackMouse} */ public void unpressContainer() { - LOGGER.trace(LoggerMarkers.Playback, "Unpressing container"); + logger.trace(LoggerMarkers.Playback, "Unpressing container"); currentPlaybackKeyboard.clear(); currentPlaybackMouse.clear(); } @@ -947,12 +951,12 @@ public void onClientPacket(PacketID id, ByteBuffer buf, String username) throws } catch (PlaybackSaveException e) { if (mc.world != null) mc.ingameGUI.getChatGUI().printChatMessage(new TextComponentString(TextFormatting.RED + e.getMessage())); - LOGGER.catching(e); + logger.catching(e); return; } catch (Exception e) { if (mc.world != null) mc.ingameGUI.getChatGUI().printChatMessage(new TextComponentString(TextFormatting.RED + "Saving failed, something went very wrong")); - LOGGER.catching(e); + logger.catching(e); return; } @@ -961,7 +965,7 @@ public void onClientPacket(PacketID id, ByteBuffer buf, String username) throws confirm.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/folder tasfiles")); mc.ingameGUI.getChatGUI().printChatMessage(confirm); } else - LOGGER.debug(LoggerMarkers.Playback, "Saved inputs to " + name + ".mctas"); + logger.debug(LoggerMarkers.Playback, "Saved inputs to " + name + ".mctas"); break; case PLAYBACK_LOAD: @@ -975,19 +979,19 @@ public void onClientPacket(PacketID id, ByteBuffer buf, String username) throws TextComponentString textComponent = new TextComponentString(e.getMessage()); mc.ingameGUI.getChatGUI().printChatMessage(textComponent); } - LOGGER.catching(e); + logger.catching(e); return; } catch (Exception e) { if (mc.world != null) mc.ingameGUI.getChatGUI().printChatMessage(new TextComponentString(TextFormatting.RED + "Loading failed, something went very wrong")); - LOGGER.catching(e); + logger.catching(e); return; } if (mc.world != null) mc.ingameGUI.getChatGUI().printChatMessage(new TextComponentString(TextFormatting.GREEN + "Loaded inputs from " + name + ".mctas")); else - LOGGER.debug(LoggerMarkers.Playback, "Loaded inputs from " + name + ".mctas"); + logger.debug(LoggerMarkers.Playback, "Loaded inputs from " + name + ".mctas"); break; case PLAYBACK_FULLPLAY: @@ -1053,7 +1057,7 @@ public void onClientPacket(PacketID id, ByteBuffer buf, String username) throws if (Minecraft.getMinecraft().world != null) Minecraft.getMinecraft().ingameGUI.getChatGUI().printChatMessage(new TextComponentString(message)); else - LOGGER.debug(LoggerMarkers.Playback, message); + logger.debug(LoggerMarkers.Playback, message); } } diff --git a/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java b/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java index 023e39a7..c4bd6559 100644 --- a/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java +++ b/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java @@ -129,7 +129,7 @@ public void recordNull(long tick) { @Override public void onPlayback(long tick, InputContainer inputContainer) { - currentValues = get(tick - 1); + currentValues = get(tick); } private MonitorContainer loadFromFile(long tick, String[] args) throws PlaybackLoadException { diff --git a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java index b4283502..7f868932 100644 --- a/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java +++ b/src/main/java/com/minecrafttas/tasmod/virtual/VirtualInput.java @@ -202,9 +202,8 @@ public void clearCurrent() { /** * Preloads the nextInputs into the currentInputs */ - public InputContainer preloadInputs() { + public void preloadInputs() { preloadInputs(KEYBOARD.nextKeyboard, MOUSE.nextMouse, CAMERA_ANGLE.nextCameraAngle); - return new InputContainer(KEYBOARD.nextKeyboard.clone(), MOUSE.nextMouse.clone(), CAMERA_ANGLE.nextCameraAngle.clone()); } public void preloadInputs(InputContainer inputContainer) { @@ -228,6 +227,9 @@ public void preloadInputs(VirtualKeyboard keyboardToPreload, VirtualMouse mouseT MOUSE.nextMouseTick(); // Preload vanilla inputs + if (Minecraft.getMinecraft() == null) { // If running in unit test env + return; + } Minecraft.getMinecraft().runTickKeyboard(); // Letting mouse and keyboard tick once to load inputs into the "currentKeyboard" Minecraft.getMinecraft().runTickMouse(); @@ -382,6 +384,10 @@ public boolean nextKeyboardSubtick() { return isPolled; } + public VirtualKeyboardEvent getCurrentEvent() { + return currentKeyboardEvent; + } + /** * @return The keycode of {@link #currentKeyboardEvent} */ @@ -562,6 +568,10 @@ public boolean nextMouseSubtick() { return isPolled; } + public VirtualMouseEvent getCurrentEvent() { + return currentMouseEvent; + } + /** * @return The keycode of {@link #currentMouseEvent} */ @@ -776,6 +786,10 @@ public void setCamera(Float pitch, Float yaw) { nextCameraAngle.set(pitch, yaw); } + public VirtualCameraAngle getCurrentCameraAngle() { + return currentCameraAngle; + } + /** * @return The current pitch coordinate of the player. May be null when it's initialized */ diff --git a/src/test/java/tasmod/TestUtil.java b/src/test/java/tasmod/TestUtil.java new file mode 100644 index 00000000..7bd718c3 --- /dev/null +++ b/src/test/java/tasmod/TestUtil.java @@ -0,0 +1,8 @@ +package tasmod; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TestUtil { + public static Logger LOGGER = LogManager.getLogger("TASmod TEST"); +} diff --git a/src/test/java/tasmod/playback/PlaybackControllerClientTest.java b/src/test/java/tasmod/playback/PlaybackControllerClientTest.java new file mode 100644 index 00000000..133d718c --- /dev/null +++ b/src/test/java/tasmod/playback/PlaybackControllerClientTest.java @@ -0,0 +1,311 @@ +package tasmod.playback; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +import java.nio.file.Paths; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.dselent.bigarraylist.BigArrayList; +import com.minecrafttas.mctcommon.events.EventListenerRegistry; +import com.minecrafttas.tasmod.playback.PlaybackControllerClient; +import com.minecrafttas.tasmod.playback.PlaybackControllerClient.InputContainer; +import com.minecrafttas.tasmod.playback.PlaybackControllerClient.TASstate; +import com.minecrafttas.tasmod.virtual.VirtualCameraAngle; +import com.minecrafttas.tasmod.virtual.VirtualInput; +import com.minecrafttas.tasmod.virtual.VirtualKey; +import com.minecrafttas.tasmod.virtual.VirtualKeyboard; +import com.minecrafttas.tasmod.virtual.VirtualMouse; +import com.minecrafttas.tasmod.virtual.event.VirtualKeyboardEvent; +import com.minecrafttas.tasmod.virtual.event.VirtualMouseEvent; + +import tasmod.TestUtil; + +class PlaybackControllerClientTest { + + VirtualInput input; + PlaybackControllerClient controller; + + @BeforeEach + void setUp() throws Exception { + input = new VirtualInput(TestUtil.LOGGER); + controller = new PlaybackControllerClient(input, Paths.get("src/test/resources/temp"), TestUtil.LOGGER); + EventListenerRegistry.register(controller); + } + + @AfterEach + void tearDown() { + EventListenerRegistry.unregister(controller); + } + + /** + * Testing if the initial input when starting a recording is correctly set + */ + @Test + void testStartRecord() { + input.KEYBOARD.updateNextKeyboard(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + input.KEYBOARD.updateNextKeyboard(VirtualKey.W.getKeycode(), true, 'w'); + controller.setTASStateClient(TASstate.RECORDING, false); + + InputContainer actual = controller.getInputs().get(0); + + VirtualKeyboard keyboard = new VirtualKeyboard(); + keyboard.updateFromEvent(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + keyboard.updateFromEvent(VirtualKey.W.getKeycode(), true, 'w'); + InputContainer expected = new InputContainer(keyboard, new VirtualMouse(), new VirtualCameraAngle()); + + assertEquals(expected, actual); + } + + @Test + void testRecord() { + controller.setTASStateClient(TASstate.RECORDING, false); + + // Tick 1 + + input.KEYBOARD.updateNextKeyboard(VirtualKey.W.getKeycode(), true, 'w'); + input.KEYBOARD.updateNextKeyboard(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + input.KEYBOARD.updateNextKeyboard(VirtualKey.LSHIFT.getKeycode(), false, '\0'); + input.MOUSE.updateNextMouse(VirtualKey.LC.getKeycode(), true, 0, 0, 0); + input.MOUSE.updateNextMouse(VirtualKey.LC.getKeycode(), false, 0, 0, 0); + input.CAMERA_ANGLE.setCamera(0F, 15F); + + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + input.CAMERA_ANGLE.nextCameraTick(); + controller.onClientTickPost(null); + + // Tick 2 + + input.KEYBOARD.updateNextKeyboard(VirtualKey.D.getKeycode(), true, 'd'); + input.MOUSE.updateNextMouse(VirtualKey.MOUSEMOVED.getKeycode(), false, 20, 3, 13); + input.CAMERA_ANGLE.setCamera(5f, 18f); + input.CAMERA_ANGLE.updateNextCameraAngle(6, 19); + input.CAMERA_ANGLE.updateNextCameraAngle(7, 20); + + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + input.CAMERA_ANGLE.nextCameraTick(); + controller.onClientTickPost(null); + + // Tick 3 + + input.KEYBOARD.updateNextKeyboard(VirtualKey.W.getKeycode(), false, '\0'); + input.MOUSE.updateNextMouse(VirtualKey.MOUSEMOVED.getKeycode(), false, -20, 0, 0); + input.CAMERA_ANGLE.setCamera(0f, 0f); + + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + input.CAMERA_ANGLE.nextCameraTick(); + controller.onClientTickPost(null); + + controller.setTASStateClient(TASstate.NONE); + + BigArrayList actual = controller.getInputs(); + + // Expected + + BigArrayList expected = new BigArrayList<>(); + + // Tick 0 + expected.add(new InputContainer()); + + // Tick 1 + VirtualKeyboard keyboard1 = new VirtualKeyboard(); + VirtualMouse mouse1 = new VirtualMouse(); + VirtualCameraAngle cameraAngle1 = new VirtualCameraAngle(); + + keyboard1.updateFromEvent(VirtualKey.W.getKeycode(), true, 'w'); + keyboard1.updateFromEvent(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + keyboard1.updateFromEvent(VirtualKey.LSHIFT.getKeycode(), false, '\0'); + mouse1.updateFromEvent(VirtualKey.LC.getKeycode(), true, 0, 0, 0); + mouse1.updateFromEvent(VirtualKey.LC.getKeycode(), false, 0, 0, 0); + cameraAngle1.set(0, 15); + + expected.add(new InputContainer(keyboard1, mouse1, cameraAngle1)); + + // Tick 2 + + VirtualKeyboard keyboard2 = new VirtualKeyboard(); + VirtualMouse mouse2 = new VirtualMouse(); + VirtualCameraAngle cameraAngle2 = new VirtualCameraAngle(); + + keyboard2.updateFromEvent(VirtualKey.D.getKeycode(), true, 'd'); + mouse2.updateFromEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, 20, 3, 13); + cameraAngle2.set(5, 18); + cameraAngle2.updateFromEvent(6, 19); + cameraAngle2.updateFromEvent(7, 20); + + expected.add(new InputContainer(keyboard2, mouse2, cameraAngle2)); + + // Tick 3 + + VirtualKeyboard keyboard3 = new VirtualKeyboard(); + VirtualMouse mouse3 = new VirtualMouse(); + VirtualCameraAngle cameraAngle3 = new VirtualCameraAngle(); + + keyboard3.updateFromEvent(VirtualKey.W.getKeycode(), false, '\0'); + mouse3.updateFromEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, -20, 0, 0); + cameraAngle3.set(0, 0); + + expected.add(new InputContainer(keyboard3, mouse3, cameraAngle3)); + + assertIterableEquals(expected, actual); + } + + @Test + void testPlayback() { + // Expected + + BigArrayList data = new BigArrayList<>(); + + // Tick 0 + data.add(new InputContainer()); + + // Tick 1 + VirtualKeyboard keyboard1 = new VirtualKeyboard(); + VirtualMouse mouse1 = new VirtualMouse(); + VirtualCameraAngle cameraAngle1 = new VirtualCameraAngle(); + + keyboard1.updateFromEvent(VirtualKey.W.getKeycode(), true, 'w'); + keyboard1.updateFromEvent(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + keyboard1.updateFromEvent(VirtualKey.LSHIFT.getKeycode(), false, '\0'); + mouse1.updateFromEvent(VirtualKey.LC.getKeycode(), true, 0, 0, 0); + mouse1.updateFromEvent(VirtualKey.LC.getKeycode(), false, 0, 0, 0); + cameraAngle1.set(0, 15); + + data.add(new InputContainer(keyboard1, mouse1, cameraAngle1)); + + // Tick 2 + + VirtualKeyboard keyboard2 = new VirtualKeyboard(); + VirtualMouse mouse2 = new VirtualMouse(); + VirtualCameraAngle cameraAngle2 = new VirtualCameraAngle(); + + keyboard2.updateFromEvent(VirtualKey.D.getKeycode(), true, 'd'); + mouse2.updateFromEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, 20, 3, 13); + cameraAngle2.updateFromEvent(5, 18); + cameraAngle2.updateFromEvent(6, 19); + cameraAngle2.updateFromEvent(7, 20); + + data.add(new InputContainer(keyboard2, mouse2, cameraAngle2)); + + // Tick 3 + + VirtualKeyboard keyboard3 = new VirtualKeyboard(); + VirtualMouse mouse3 = new VirtualMouse(); + VirtualCameraAngle cameraAngle3 = new VirtualCameraAngle(); + + keyboard3.updateFromEvent(VirtualKey.W.getKeycode(), false, '\0'); + mouse3.updateFromEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, -20, 0, 0); + cameraAngle3.set(0, 0); + + data.add(new InputContainer(keyboard3, mouse3, cameraAngle3)); + + // Test + + controller.setInputs(data); + + controller.setTASStateClient(TASstate.PLAYBACK); + + // Tick 1 + controller.onClientTickPre(null); + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + + VirtualKeyboardEvent expectedK; + VirtualMouseEvent expectedM; + + // Subtick 1 + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.W.getKeycode(), true, 'w'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + input.MOUSE.nextMouseSubtick(); + expectedM = new VirtualMouseEvent(VirtualKey.LC.getKeycode(), true, 0, 0, 0); + assertEquals(expectedM, input.MOUSE.getCurrentEvent()); + + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC1 = new VirtualCameraAngle(0f, 15f); + assertEquals(expectedC1, input.CAMERA_ANGLE.getCurrentCameraAngle()); + + // Subtick 2 + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.LSHIFT.getKeycode(), true, '\0'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + input.MOUSE.nextMouseSubtick(); + expectedM = new VirtualMouseEvent(VirtualKey.LC.getKeycode(), false, 0, 0, 0); + assertEquals(expectedM, input.MOUSE.getCurrentEvent()); + + // Subtick 3 + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.LSHIFT.getKeycode(), false, '\0'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + // Tick 2 + controller.onClientTickPre(null); + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + + // Subtick 1 + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.W.getKeycode(), false, '\0'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + input.MOUSE.nextMouseSubtick(); + expectedM = new VirtualMouseEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, 20, 3, 13); + assertEquals(expectedM, input.MOUSE.getCurrentEvent()); + + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC2 = new VirtualCameraAngle(); + expectedC2.deepCopyFrom(expectedC1); + expectedC2.updateFromEvent(5f, 18f); + assertEquals(expectedC2, input.CAMERA_ANGLE.getCurrentCameraAngle()); + + // Subtick 2 + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.D.getKeycode(), true, 'd'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC3 = new VirtualCameraAngle(); + expectedC3.deepCopyFrom(expectedC2); + expectedC3.updateFromEvent(6f, 19f); + assertEquals(expectedC3, input.CAMERA_ANGLE.getCurrentCameraAngle()); + + // Subtick 3 + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC4 = new VirtualCameraAngle(); + expectedC4.deepCopyFrom(expectedC3); + expectedC4.updateFromEvent(6f, 19f); + assertEquals(expectedC4, input.CAMERA_ANGLE.getCurrentCameraAngle()); + + // Subtick 4 + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC5 = new VirtualCameraAngle(); + expectedC5.deepCopyFrom(expectedC4); + expectedC5.updateFromEvent(7f, 20f); + assertEquals(expectedC5, input.CAMERA_ANGLE.getCurrentCameraAngle()); + + // Tick 3 + controller.onClientTickPre(null); + input.KEYBOARD.nextKeyboardTick(); + input.MOUSE.nextMouseTick(); + + input.KEYBOARD.nextKeyboardSubtick(); + expectedK = new VirtualKeyboardEvent(VirtualKey.D.getKeycode(), false, '\0'); + assertEquals(expectedK, input.KEYBOARD.getCurrentEvent()); + + input.MOUSE.nextMouseSubtick(); + expectedM = new VirtualMouseEvent(VirtualKey.MOUSEMOVED.getKeycode(), false, -20, 0, 0); + assertEquals(expectedM, input.MOUSE.getCurrentEvent()); + + input.CAMERA_ANGLE.nextCameraTick(); + VirtualCameraAngle expectedC6 = new VirtualCameraAngle(0f, 0f); + assertEquals(expectedC6, input.CAMERA_ANGLE.getCurrentCameraAngle()); + } +} From f5ebdc9c69117ef0be09961ba57e11c3632f6064 Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 16 Apr 2026 11:17:25 +0200 Subject: [PATCH 4/4] [Playback] Fix camera angle desyncing during playback --- .../java/com/minecrafttas/tasmod/mixin/MixinMinecraft.java | 2 ++ .../tasmod/mixin/playbackhooks/MixinMinecraft.java | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/MixinMinecraft.java b/src/main/java/com/minecrafttas/tasmod/mixin/MixinMinecraft.java index 450efdc7..9b8115e9 100644 --- a/src/main/java/com/minecrafttas/tasmod/mixin/MixinMinecraft.java +++ b/src/main/java/com/minecrafttas/tasmod/mixin/MixinMinecraft.java @@ -11,6 +11,7 @@ import com.minecrafttas.mctcommon.events.EventListenerRegistry; import com.minecrafttas.tasmod.TASmodClient; +import com.minecrafttas.tasmod.events.EventClient; import com.minecrafttas.tasmod.events.EventClient.EventClientTickPost; import com.minecrafttas.tasmod.util.Ducks.SubtickDuck; @@ -45,6 +46,7 @@ public void injectRunGameLoop(CallbackInfo ci) { @Redirect(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runTick()V")) public void redirectRunTick(Minecraft mc) { + EventListenerRegistry.fireEvent(EventClient.EventClientTickPre.class, (Minecraft) (Object) this); if (TASmodClient.tickratechanger.ticksPerSecond != 0) { ((SubtickDuck) this.entityRenderer).runUpdate(this.isGamePaused ? this.renderPartialTicksPaused : this.timer.renderPartialTicks); } diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java b/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java index 8a0a87e5..35bad375 100644 --- a/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java +++ b/src/main/java/com/minecrafttas/tasmod/mixin/playbackhooks/MixinMinecraft.java @@ -9,9 +9,7 @@ import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import com.minecrafttas.mctcommon.events.EventListenerRegistry; import com.minecrafttas.tasmod.TASmodClient; -import com.minecrafttas.tasmod.events.EventClient; import com.minecrafttas.tasmod.virtual.SubtickGuiScreen; import com.minecrafttas.tasmod.virtual.VirtualInput; import com.minecrafttas.tasmod.virtual.VirtualInput.VirtualKeyboardInput; @@ -50,7 +48,6 @@ public void playback_injectRunGameLoop(CallbackInfo ci) { */ @Inject(method = "runTick", at = @At(value = "HEAD")) public void playback_injectRunTick(CallbackInfo ci) { - EventListenerRegistry.fireEvent(EventClient.EventClientTickPre.class, (Minecraft) (Object) this); // Fire this *before* nextKeyboardTick and nextMouseTick, or playing back ticks will desync! /* * Both of these were previously in injectRunTickKeyboard/Mouse * but moved to the beginning of runTick to fix #224