Dilli, I’d like to get off Mr. Bones wild ride
- Jake Logan, probably
Glide wrappers can be both a savior and a menace sometimes. The game looks great with them:
But on every start of a new level you get uncontrollable spinning of your ship. The spinning looks to be always on a \ diagonal - so what's going on? Can we fix that? Let's open the game up in ghidra and see what we can figure out. The issue must come from some faulty cursor logic, right? My initial gut reaction was to trace DxInput:
The game for some reason sets up two dxinput interfaces. I looked around the two caller functions and it seems the second one is specifically for joystick input, while the first one enumerates devices and sets callbacks. That didn't help me either way. Ok, what about functions that deal with the cursor? Are there any?
Bingo! That ClipCursor and SetCursorPos are readily good contenders for where bugs around spinning ship might happen. ClipCursor restricts cursor movement to a rectangle, and SetCursorPos moves the cursor where the game wants it to be.
By the time I took the screenshots, I already went and explored some of the functions that call these functions - that's why They have reasonable names. You'll see nothing but FUN_XXXXXX if you just opened space.exe for the first time in ghidra. But those functions are decently small and don't refer to many static variables, which should be somewhat straightforward to figure out what they do. To be honest, I still don't fully understand this system. It appears all of UI is hard-coded to be at 640x480, including mouse pointer lock. Then during the game, window sizes are taken into account and numbers are adjusted accordingly.
If you trace those ClipCursorToX and ExpandCursorPosition functions you'll find that They are used in the main window process function:
Quick shout out to pinvoke.net - that's the cleanest source of Windows' constants I was able to find. We see that when the message is 0x1c - it's WM_ACTIVATEAPP message. And reading the documentation for WM_ACTIVATEAPP we see that when wParam is false, the window is being deactivated, when it's true - activated. There's a layer of indirection here - I guess the code was meant to substitute different activation/deactivation handlers, but lukcly current addresses in the binary point to exact functions used
And it just so happens that these are the functions that call our ClipCursor eventually:
Ok, so for those who are more familiar with the bug - the bug gets cleared when we alt-tab from the game and go back in - to it's these two functions - onWindowActivate at address 0x432d80 and onWindowDeactivate at address 0x432dc0 that end up "fixing" the bug.
I spent some more time looking at the SetCursorPos function and who calls it. This allowed me to uncover the static structure in the game that relates to all things mouse:
I use my own little convention of adding _maybe to functions or data that I'm not certain used exactly so. You'll also notice a lot of green labels from Ghidra - that's an indication of static data. E.g. CONFIG_CURSOR.pos.x is at 0x603e40 and y is at 0x603e44 - you should be able to add these addresses in something like Cheat Engine to see the mouse coordinates the game using right now.
From there I was able to find this really interesting function - and that's how the game actually gets mouse position data to begin with:
Only one function ever calls this PeekMouseMessages, and only one more function calls it. I spent some time exploring those functions. It's good to trace not only up and down the stack but also to look how some data gets moved around and what writes to this data. By a total accident from a different effort I ended up finding how the game reads config files and stores their data in memory. From there I've already touched this value before:
What is this MOUSE_NO_TOGGLE? The game reads it from a config file and if I grep -irn NO_TOGGLE inside of the game directory I get a single hit to the readme:
======================================================================So the function that calls our PeekMouseMessages() is actually Mouse_FineMoveControls()! And it looks like this once you rename some statics:
6. Corrections to the Manual:
======================================================================
* Mouse Controls: The mouse controls have been enhanced for
easier use. The mouse defaults to Fine Look controls (formerly
you had to hold down right mouse button for this mode). Pressing
the right mouse button now toggles between Fine Control and Fast
Turning Control. You do not have to hold down the right mouse
button anymore to stay in either mode. If you prefer the style
detailed in the manual and keychart, simply open up the
Tachyon.cfg file with WordPad. Under the [CONTROL] section,
change MOUSE_NO_TOGGLE=0 to MOUSE_NO_TOGGLE=1, then save the
file.
More over the sole caller is:
So at this point I sat there for a while looking for bugs in any of these functions. There's some edge-case between how the game clips the cursor and gets the mouse coordinates that makes the ship spin uncontrollably... but only with a glide wrapper, and only with some of them... I don't know all things glide wrappers do and I didn't want to spend time understanding them either... But I did remember how alt-tabbing fixes the issue... And everything works fine in the menus - only after level load something messed up happens. Perhaps that's when screen resolution changes? What if we just force the game to recalculate clip region after a level loads? Luckily, from a previous effort I've already identified function that switches from loading screen to playing the game:
This is a function located at 0x44ec80 in Steam version of the game. All we need to do is call a function that re-calculates cursor clipping coordinates between showing the loading screen and playing a level... And luckily we already have a function that does that - onWindowActivate. I've patched a jump to some unused executable memory first after the return of the loading screen call and added a call to onWindowActivate there. And suddenly there was no more spinning!
No comments:
Post a Comment