diff --git a/src/main/java/com/minecrafttas/tasmod/TASmodClient.java b/src/main/java/com/minecrafttas/tasmod/TASmodClient.java index 8dc8ec4c..177b830f 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, 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/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/playback/PlaybackControllerClient.java b/src/main/java/com/minecrafttas/tasmod/playback/PlaybackControllerClient.java index 6c48a0ba..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; @@ -34,9 +33,9 @@ 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.EventClientTickPre; import com.minecrafttas.tasmod.events.EventClient.EventDrawScreen; import com.minecrafttas.tasmod.events.EventPlaybackClient; import com.minecrafttas.tasmod.events.EventPlaybackClient.EventControllerStateChange; @@ -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; @@ -93,6 +92,7 @@ public class PlaybackControllerClient implements ClientPacketHandler, EventClientInit, + EventClientTickPre, EventClientTickPost, EventDrawScreen, @@ -102,7 +102,7 @@ public class PlaybackControllerClient implements //@formatter:on { - private Logger logger = TASmod.LOGGER; + private final Logger logger; /** * The current state of the controller. @@ -119,6 +119,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 +176,10 @@ public class PlaybackControllerClient implements // ===================================================================================================== - public PlaybackControllerClient() { - tasFileDirectory = TASmodClient.tasfiledirectory; - + public PlaybackControllerClient(VirtualInput virtual, Path tasFileDirectory, Logger logger) { + this.virtual = virtual; + this.logger = logger; + this.tasFileDirectory = tasFileDirectory; inputs = new BigArrayList(tasFileDirectory.resolve("temp").toAbsolutePath().toString()); } @@ -230,11 +236,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" : ""; @@ -248,13 +252,12 @@ 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" : ""; 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 +267,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"); + 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); @@ -280,58 +282,66 @@ 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 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 ._."; } private void startRecording() { - LOGGER.debug(LoggerMarkers.Playback, "Starting recording"); + 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()); + 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"); - TASmodClient.virtual.clearNext(); + logger.debug(LoggerMarkers.Playback, "Stopping a recording"); + virtual.clearNext(); + state = TASstate.NONE; } private void startPlayback() { - LOGGER.debug(LoggerMarkers.Playback, "Starting playback"); - Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 + logger.debug(LoggerMarkers.Playback, "Starting playback"); + state = TASstate.PLAYBACK; + + InputContainer initialContainer = inputs.get(0); + this.currentPlaybackKeyboard = initialContainer.getKeyboard().clone(); + this.currentPlaybackMouse = initialContainer.getMouse().clone(); + this.currentPlaybackCameraAngle = initialContainer.getCameraAngle().clone(); + virtual.preloadInputs(initialContainer); + + if (Minecraft.getMinecraft() != null) + Minecraft.getMinecraft().gameSettings.chatLinks = false; // #119 index = 0; -// TASmod.ktrngHandler.setInitialSeed(startSeed); } 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) { - TASmodClient.virtual.clearNext(); + virtual.clearNext(); } + state = TASstate.NONE; } /** @@ -354,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); @@ -445,51 +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(); } -// if (TASmod.isDevEnvironment) { -// 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 { @@ -499,46 +504,50 @@ 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; } - index++; // Increase the index and load the next inputs - - EventListenerRegistry.fireEvent(EventPlaybackTickPre.class, index); - /* Stop condition */ - if (index == inputs.size() || inputs.isEmpty()) { - index--; + 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(); - } - EventListenerRegistry.fireEvent(EventPlaybackTick.class, index, container); + if (index + 1 < inputs.size()) { + InputContainer nextContainer = inputs.get(index + 1); + + this.nextPlaybackKeyboard = nextContainer.getKeyboard().clone(); + this.nextPlaybackMouse = nextContainer.getMouse().clone(); + this.nextPlaybackCameraAngle = nextContainer.getCameraAngle().clone(); + } else { + 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); } // ===================================================================================================== // Methods to manipulate inputs @@ -606,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); @@ -663,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(); } @@ -942,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; } @@ -956,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: @@ -970,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: @@ -1048,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/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/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java b/src/main/java/com/minecrafttas/tasmod/playback/filecommands/builtin/DesyncMonitorFileCommandExtension.java index 1a383e98..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 @@ -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; } @@ -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/registries/TASmodKeybinds.java b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java index bbd88526..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,6 +48,11 @@ public enum TASmodKeybinds implements KeybindID { TASmodClient.virtual.CAMERA_ANGLE.updateNextCameraAngle(0, 45); }), TEST1("Various Testing", "TASmod", Keyboard.KEY_F12, () -> { + 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 24e23965..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.preloadInput(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/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..7f868932 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,24 @@ public void clearCurrent() { MOUSE.clearCurrent(); } - public void preloadInput(VirtualKeyboard keyboardToPreload, VirtualMouse mouseToPreload, VirtualCameraAngle angleToPreload) { + /** + * Preloads the nextInputs into the currentInputs + */ + public void preloadInputs() { + preloadInputs(KEYBOARD.nextKeyboard, MOUSE.nextMouse, CAMERA_ANGLE.nextCameraAngle); + } + + 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 + * @param mouseToPreload + * @param angleToPreload + */ + public void preloadInputs(VirtualKeyboard keyboardToPreload, VirtualMouse mouseToPreload, VirtualCameraAngle angleToPreload) { // Preload the nextKeyboard KEYBOARD.nextKeyboard.deepCopyFrom(keyboardToPreload); MOUSE.nextMouse.deepCopyFrom(mouseToPreload); @@ -209,6 +227,9 @@ public void preloadInput(VirtualKeyboard keyboardToPreload, VirtualMouse mouseTo 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(); @@ -363,6 +384,10 @@ public boolean nextKeyboardSubtick() { return isPolled; } + public VirtualKeyboardEvent getCurrentEvent() { + return currentKeyboardEvent; + } + /** * @return The keycode of {@link #currentKeyboardEvent} */ @@ -543,6 +568,10 @@ public boolean nextMouseSubtick() { return isPolled; } + public VirtualMouseEvent getCurrentEvent() { + return currentMouseEvent; + } + /** * @return The keycode of {@link #currentMouseEvent} */ @@ -757,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/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", 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()); + } +}