Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ghost Control Entry Parser for analog steering #15

Open
TMDarky opened this issue Jan 17, 2021 · 12 comments
Open

Ghost Control Entry Parser for analog steering #15

TMDarky opened this issue Jan 17, 2021 · 12 comments
Assignees

Comments

@TMDarky
Copy link

TMDarky commented Jan 17, 2021

The code assumes any non-zero value means the button is "On".
This is mostly correct, but is incomplete.

result.OnOffU = reader.ReadUInt32();
public bool OnOff => this.OnOffU > 0;

For normal buttons, the value is 0 (Off) or 0x80 (On).

For analog controls, the value is between -65536 and +65536 (0xFF0000 , 0x010000).
This is an Int24 followed by 8 unused bits as far as I can tell (always 0x00).

The control "Steer" is often used and the extracted data isn't easy to parse as the values are encoded as an Int24 inside 32 bits (OnOffU).

Extra analog details

Steer (if button _FakeDontInverseAxis is On) : negative values turn left, positive values turn right. 0 indicates no steering.
Steer (if button _FakeDontInverseAxis is not On (i.e. not present at all) : negative values turn right, positive values turn left. 0 indicates no steering.
Gas : negative values brake (reverse), positive values accelerate. 0 indicates no brakes/gas.
AccelerateReal : analog acceleration.
BrakeReal : analog braking.

For AccelerateReal and BrakeReal, the game seems to treat all values between 0xFF0000 and ~0x004E00 as Off, and everything else (until 0x010000) as On. They are set as 0xFF0000 at the beginning of the race.
For Gas, the same is true: [~0xFFB200, ~0x004E00] is no brake/acceleration. < ~0xFFB200 is full braking, > ~0x004E00 is full acceleration.
I'm adding tildes everywhere because I couldn't pinpoint the exact value, but it's somewhere around there.

As a result, the analog acceleration and brakes are not really analog as they map only to On and Off.

@donadigo
Copy link
Collaborator

donadigo commented Jan 17, 2021

I'm not sure what version of the game you're talking about. I'm assuming TM2/TMUF. You're almost correct, but there's some errors:
for both Steer and Gas events, the data looks like this:

uint16 steer;
uint8 unused;
uint8 direction;

steer represents the amount of steer at that time point. It's unsigned and ranges from 0 to 65535. This is fairly simple.
unused is simply unused byte.
direction or flags whatever you call it, determines how steer is interpreted:

  • if it is 0xFF then the steer is to the right
  • if it is 0 then the steer is to the left
  • if it is 0x1 then the steer is full left (steer is 0 in this case)
  • any other direction value than this is not possible to emit by the game normally, however it's actually a continuous value that can be changed to e.g 0x2 to cause the car to steer twice as strong to the left than the maximum range.

@TMDarky
Copy link
Author

TMDarky commented Jan 17, 2021

Indeed, TM2 & TMUF.

Having a special rule "0x1 represents full left steer" makes a lot less sense than simply interpreting the 3 first bytes as an Int24.

Also, nearly full left steer is, in your structure, steer = 0xFFFF, direction = 0x00 with FFFF being 65535.
However, nearly full right steer is steer = 0x0001, direction = 0xFF, with 0001 being... 1 ?
When going right, it goes from FFFF to 0000... It only makes sense as an int24, going from FFFFFF to FF0000.

Interpreting it as an Int explains why direction = 0x2 doubles the steering.

@donadigo
Copy link
Collaborator

donadigo commented Jan 17, 2021

There is simply no counterpart for "full right steer". If you were to inspect memory in-game you would see that steering full right on the pad does not switch flags, only left has this behaviour. As for why this is the case, I don't know, but replaying inputs for tons of replays has never failed me for interpreting this structure like this. I actually have to revise this before being sure.

@TMDarky
Copy link
Author

TMDarky commented Jan 17, 2021

We both get the same results...
However you want to interpret the data, this library is not properly parsing analog commands.

BTW I just noticed it's thanks to your trackmania TAS video (What now?) I started looking into this.

@stefan-baumann
Copy link
Owner

@TMDarky Thank you for that info and opening the issue! I was actually just thinking about that a few days ago and was about to think about good ways of implementing it.
My implementation was based on the reference information from the ManiaTech Wiki which obviously was/is incorrect.
I'll try and take a look into this in more depth in the coming days and update you here when I make progress. I'll also try and see if I can find the actual logic from the game in the code to make an implementation that is as correct as possible.

@stefan-baumann
Copy link
Owner

Okay, I looked into the disassembly of the game executable and it seems like the data is indeed encoded as 24-bit segments as you said @TMDarky. I think I actually found the method that interprets those values too, but it'll take me some time to fully understand how it works.

@donadigo
Copy link
Collaborator

For reference, here's a demonstration of what value pairs lead to what steering in-game: https://streamable.com/pghixl. I hope this clears things a little bit and helps with the development.

@TMDarky
Copy link
Author

TMDarky commented Jan 19, 2021

@donadigo could you check at what exact value gas gets turned on ? (both forwards and backwards)
MP# could that way precisely interpret it and return on or off without displaying the (fake) analog value. If that's a thing stefan-baumann wants to implement.

It should be at around 0x004E00 (your structure : 20000 0) for acceleration and 0xFFB200 (~40000 255 ?) for brakes

@stefan-baumann
Copy link
Owner

stefan-baumann commented Jan 19, 2021

@TMDarky Looking into the disassembly, some values seem to be interpreted as floats. So those approximate values you mention might actually make more sense when interpreted as such.

Regarding precise interpretation of all the values: I'd love to implement that, especially if it's useful for someone. If you're interested in looking into how it functions internally, I can also give you access to some code I decompiled from the game executable which I'll look at more thoroughly in the coming days.

One other thing to note: It seems that while TMNF/TMUF and TM2 encode those values in the same way, there has apparently been a change in TM2020, where those values are now encoded slightly differently. I'll try and look into how we can parse both of the formats, too.

@donadigo
Copy link
Collaborator

donadigo commented Jan 20, 2021

@TMDarky the exact value (or offset) at which acceleration triggers is 19661 0. The offset for brakes is the same only that it is inverted 45875 255 (65536 - 19661 = 45875).

@donadigo
Copy link
Collaborator

I revised my previous assumptions and code as well and of course, I was wrong. I also have some new insights that should help reversing the code further.

I wrongly assumed that 65535 0 pair does not exist, and instead it's represented as 0 1. This threw me off with the math to convert between representations. The normal range ingame is perfectly balanced: [-65536, 65536], or represented with a 24 bit internal values: [(0, 1), (0, 255)]. The range can be expanded to the full 24bit value which will give us 16777216 possible values for steering. The game however instead clamps the possible range to [(0, 100), (0,156)] which gives us the final range of [-6553600, 6553600] which actually affects ingame physics. Values outside this range do not affect the car behaviour anymore and at the end get clamped back and saved to the replay file.

I wrote some Python code to convert between those single values and the 24bit pair, and also incorporated it into my tools:

def analog_value_to_state(value):
    axis = 0
    flags = 0
    if value < 0:
        mul = int(value / 65536)
        flags = -mul
        axis = -value + (mul * 65536)
    elif value > 0:
        value -= 1
        mul = int(value / 65536)
        flags = 255 - mul
        value -= (mul * 65536)
        axis = 65535 - value

    return axis, flags


def state_to_analog_value(axis, flags):
    if flags < 128:
        return -axis - (flags * 65536)
    else:
        mul = 255 - flags
        return (mul * 65536) + (65536 - axis)

And verifying that the algorithm covers the full 24bit range:

for val in range(-8388607, 8388608 + 1):
    axis, flags = analog_value_to_state(val)
    assert(state_to_analog_value(axis, flags) == val)

Regarding TM2020, I've got some info that, because the game engine is now entirely based on Shootmania, the inputs saved in the file could be more "generalized" inputs, rather than values strictly related to car control. This needs further investigation.

@TMDarky
Copy link
Author

TMDarky commented Jan 21, 2021

Interesting stuff, thanks for the info @donadigo !
Weird that they'd clamp at 100* 2^16 but that's Nadeo for you. From your video, extreme steering seems to produce very unique effects (the car turning by itself is pretty neat).

@stefan-baumann I'm not sure I'll be able to read decompiled code, but I'd like to take a look, sure !

Repository owner deleted a comment from TMDarky Jan 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants