MinecraftTAS / TASmod

Minecraft Tool-Assisted Speedrun (TAS) Tools with input playback
https://discord.gg/jGhNxpd
GNU General Public License v3.0
30 stars 5 forks source link

Refactor VirtualInput #179

Closed ScribbleTAS closed 8 months ago

ScribbleTAS commented 1 year ago

Big redesign of the VirtualInput system, which is responsible for hooking into the vanilla input system and redirecting the inputs inbetween LWJGL and the keybindings.

Terminology

Example:

Keyboard-State:[]  
-> Keyboard-Event:W, true
Keyboard-State[W]
-> Keyboard-Event:S, true
-> Keyboard-Event:A, true
-> Keyboard-Event:D, true
Keyboard-State[W,S,A,D]
-> Keyboard-Event:A, false
Keyboard-State[W,S,D]

Current system

As it stands, LWJGL only transfers keyboard events, which update the vanilla keybinding state.
VirtualInput was therefore designed to mimic the vanilla keybinding system, by storing inputs as a state, but then being able to export events to update the vanilla keybindings.

And to better handle savestates, exporting events is done by comparing 2 states and then looking at the differences. If there are differences in the keys that are pressed, then an appropriate event can be generated from that.

Changes

Storing only pressed keys

I recently had an epiphany, which made this PR all the more necessary... Why am I storing which keys are unpressed?
Yes, you heard that right, every tick 140ish VirtualKey-Objects are created and 95% of them only store that they are currently not pressed.
In my defense, back then I had no sense of memory management and this system would've made sense if I were to store how often the key is being pressed in a tick, which ended up never happening.

So, to clean up, we now only store the keycodes of the keys that are currently pressed, in a list.

Changing the collection types

3 years ago, my past self apparently didn't know that there are more collection types in java than ArrayList and HashMap.
So, the pressed keycodes are now stored in a LinkedHashSet, because keycodes should never duplicate
and the events that update the vanilla keybindings are now stored in a Queue.

Subticks

Since we only update the keyboards every tick instead of every frame, users would notice some odd behaviour, that were unavoidable with the current system.

When playing the game in low tickrates for example, pressing the inventory key and releasing it inbetween ticks, would not be recognised by the game and the inventory would not open.
In vanilla however, the inventory would indeed open.
Other parts of the game also relied on keyboard pressed inbetween ticks. Back then I didn't know it was necessary so I didn't implement a fix... Later on I had to tack on subtick support for the mouse in the form of "paths", which worked but was in many ways a bad idea.

Now we store key presses inbetween ticks as well, which should now allow for the full input control, while still being updated only 20 times per second. This PR does not include changes to the Playback System and the TASFile, but a preview of how it will look can be seen in #164

Removing Unicode hack

Some chat functions like the arrow keys etc, did not have an associated character and instead used the keycode. This is a problem as we update the keycodes to pressed or unpressed every tick, but the chat relying on characters does not have this constrain.

And the solution I had is still something I really like, but sadly not very future proof and unwieldy to use.

I just assigned special unicode characters to each chat function! And since these symbols are usually not used in chat, you would never run into issues.

The symbols are:

Now with subticks, there are no need for these symbols, however maybe I'll add them in the new playback file format as an easteregg/visualisation, as this was somewhat helpful to discern what you were pressing.

Camera Subticks

In vanilla, the player rotation is updated every frame.
This is an issue, when keyboard and mouse are updated every tick and everything should still be synced. After a lot of testing I decided to make the player rotation update every tick. This introduced another problem, now rotating the camera was tied to the tickrate and felt like e.g. 20fps even though the framerate was usually higher.

Pancake came up with a solution. We can seperate the rotation from the player from the rotation of the camera.
The "camera" is responsible for the actual view rotation, so what you see on screen, while the player rotation is used for game logic, like at which block you are aiming at.

By updating the camera every frame and updating the player rotation every tick, the change becomes almost unnoticable.

But, only recording every tick meant that during playback, the camera was back at being 20fps. Pancake, yet again,
wrote interpolation code to smooth out the camera path.

Now with subticks on the camera, we have more control over the interpolation frames on the camera, instead of guessing we can just set the camera coordinates.

Unit testing and documentation

Finally... Do I need to say anything?

TODO

ScribbleTAS commented 8 months ago

@PancakeTAS I'm in the process of finalizing the VirtualInput2 and I thought to seek out your feedback on this.

Previously, VirtualInput had Keyboard, Mouse and Subtick (now camera angle) very confusingly mixed up... For better visibility, I added inner classes to better distinguish between them... Now VirtualInput2 has an object for each inner class.

And, for better visibility, I chose to make them public final and uppercase, to really show which part of VirtualInput you are using, instead of a getter: TASmodClient.virtual.KEYBOARD.nextTick() for example.

I was also tryharding a bit to make the relation between currentKeyboard and nextKeyboard as memory efficient as possible, by sometimes passing in queues as a reference so that they can be filled without returning a new Queue...

I'm still missing some documentation and tests for VirtualMouse2 and VirtualInput2 but I first want to see if the style of VirtualKeyboard2 is to your liking before committing on that...

As for "subticks", basically each Keyboard (or the base class VirtualPeripheral) stores all of it's changes.

Everytime update() is called, a clone is created and stored in subtickList, while the "parent" just contains the most recent change. That way I can get rid of the horrendous subtick-path stuff in VirtualMouse and add subtick functionality to VirtualKeyboard, which means that you won't need to hold the keys until the next tick anymore to get stuff applied... Horray...

Pls don't make me rewrite the whole thing

ScribbleTAS commented 8 months ago

Note to self: ignoreFirstUpdate in VirtualPeripheral is a bit faulty and can lead to missed inputs when preloading the keyboard... So it's best to make something more robust...

Idk what that robust thing is, so future me has to deal with that... Maybe an addMultiple next to addSubtick and route preloading subticks that way

ScribbleTAS commented 8 months ago

Note to self: Weird phantom press when you press the keys in this configuration eclipse_S03jNcwVIt

Solution:
Don't add char to charlist if keystate is false in update...

Also: Remove ability to set cursorX and cursorY to null and move that to PlaybackSerializer..

ScribbleTAS commented 8 months ago

Note to self: Add subtick things to VirtualCameraAngle!
Capture the camera angle in run game loop and read only the most recent element in tick function.

onCameraEvent() {
    List<VirtualCameraAngle> cameraSubticks = cameraAngle.getAll();
    pitch = cameraSubticks.get(MathHelper.clampedLerp(0, cameraSubticks.size(), renderPartialTicks).getPitch();
}

Lerps through the subtickList inbetween ticks and uses the subtickList as interpolation instead of guessing the rotation from an start and endpoint.

ScribbleTAS commented 8 months ago

image

God dammit, alright... Note to self: Switch to recording mouse deltas instead of pure camera angle coordinates, to make the system more vanilla like

I'm still scarred by the harrowing experience that I had while getting TASmod to work on MCP, which was even before this github repo... Tickrate 0 and mouse deltas just didn't match... But I think I'm a lot smarter now, so I'm gonna try this again... Might also be easier to upgrade and downgrade in the future...

ScribbleTAS commented 8 months ago

Note to self: First mouse click after joining a world is not recognized. Seems like my old issue "The loading screen doesn't fire LWGJL events" is back...
Since I leftclick to enter the world, the key release event is never fired in the loading screen... Thanks Minecraft!

ScribbleTAS commented 8 months ago

Note to self: