Bakkes / CPPRP

Fast C++ Rocket League replay parser
Mozilla Public License 2.0
32 stars 9 forks source link

How to get PlayerName from Car Actor? #14

Open davechurchill opened 1 year ago

davechurchill commented 1 year ago

I have the following code outputting data about all the cars in the game. I would like to somehow get their PlayerName from the game, but I for the life of me can't figure out how. When I index the replayFile->names array by the car's nameID, I get something like Car_TA_403 instead of the PlayerName. I have the ActorID, is there any way to get the name from that?

Would be eternally grateful if someone help me out

auto replayFile = std::make_shared<CPPRP::ReplayFile>("c:/original_good.replay");
replayFile->Load();
replayFile->DeserializeHeader();
replayFile->tickables.push_back([&](const CPPRP::Frame& f, const std::unordered_map<uint32_t, CPPRP::ActorStateData>& actorStats)
{
    std::cout << f.frameNumber << " " << f.time << "\n";
    for (auto& actor : actorStats)
    {
        std::shared_ptr<CPPRP::TAGame::Car_TA> car = std::dynamic_pointer_cast<CPPRP::TAGame::Car_TA>(actor.second.actorObject);
        if (car)
        {
            auto rbState = car->ReplicatedRBState;
            std::cout << "   Car: " << actor.second.actorId << " " << actor.second.nameId << " " << car->PlayerReplicationInfo.actor_id << " ";
            std::cout << names[actor.second.nameId] << " " << (int)car->TeamPaint.team_number << " ";
            std::cout << rbState.position.ToString() << "\n";
        }
    }
});
replayFile->Parse();
Bakkes commented 1 year ago

I agree the replay file is quite tricky to work with because you need to connect a bunch of stuff yourself. For this issue: The car actor contains info related to the car, for stuff like the player name you'd need the corresponding PRI (Player Replication Info). You should be able to do this with:

for (auto& actor : actorStats)
{
    std::shared_ptr<CPPRP::TAGame::Car_TA> car = std::dynamic_pointer_cast<CPPRP::TAGame::Car_TA>(actor.second.actorObject);
    if (car)
    {
        auto pri = replayFile->GetActiveActor<CPPRP::TAGame::PRI_TA>(car->PlayerReplicationInfo);
        if (pri)
        {
            std::cout << pri->PlayerName << '\n';
        }
        else
        {
            std::cout << "no name!\n";
        }
        auto rbState = car->ReplicatedRBState;
        std::cout << "   Car: " << actor.second.actorId << " " << actor.second.nameId << " " << car->PlayerReplicationInfo.actor_id << " \n";
        //std::cout << names[actor.second.nameId] << " " << (int)car->TeamPaint.team_number << " ";
        std::cout << rbState.position.ToString() << "\n";
    }
}

All the fields in GameClasses.h which are of type ActiveActor or ObjectTarget refer to another actor you can retrieve with GetActiveActor (but doesn't always exist so still need to nullcheck). In this case that's car->PlayerReplicationInfo. The names in the replay file aren't really related to player names, but contain things like class and field names. Player names are a property thus they're stored in the game classes themselves. Hope this helps!

davechurchill commented 1 year ago

Yes, it's been quite the learning experience trying to extract the info I want from the replays! I initially started out by using rrrocket to parse the replays to JSON and then working with the JSON to get all the details from the replay. Then I started diving into the CPPRP library and this is actually a bit easier to work with. However this solution escaped me.

One more question for you: I noticed that a couple of replays that I have do not have a "PlayerStats" attribute. Do you know why this might be? It stores such valuable information about teams, goals, etc, when it's missing I basically have to discard the replay from my analysis

Bakkes commented 1 year ago

I'm not sure why that is. My guess would be that those are replays where the uploader leaves before the game is fully over, causing a certain game end event that is responsible for writing the player stats to the header to not get fired. But that's just a hunch.

It shouldn't be too hard to reconstruct it though. The PRI_TA class stores most of the data that's in the playerstats from what I can see (matchgoals, matchassists, matchscore, matchsaves). You could just run through the replay and store the last known state for every pri, and when parsing is done use that data as the player stats.

Getting the team number is a little trickier, but I've got some code you can use for that:

After deserializeheader & before calling parse:

team0typeid = replay_->objectToId["Archetypes.Teams.Team0"];
team1typeid = replay_->objectToId["Archetypes.Teams.Team1"];

Also register an OnActorCreated callback replay->createdCallbacks.push_back(std::bind(&ReplayStateExtractor::OnActorCreated, this, _1));

void ReplayStateExtractor::OnActorCreated(const ActorStateData& createdActor)
{
    if (createdActor.typeId == team0typeid)
    {
        team0actor = createdActor.actorId;
    }
    else if (createdActor.typeId == team1typeid)
    {
        team1actor = createdActor.actorId;
    }

}

Then to find out which team a PRI belongs to:

if (pri->Team.actor_id == team0actor)
{
    //Team 0 blue
}
else if (pri->Team.actor_id == team1actor)
{
    //Team 1 orange
}
davechurchill commented 1 year ago

Thank you so much again for your help this is really saving a lot of time, if I would have even figured it out on my own.

Might I suggest adding a cleaned up version of your first reply to the README or a wiki as a great piece of example starter code for people using the library? I know some people are trying to parse rocket league replays to extract data for machine learning, and this seems like the most efficient way to do it, since all the other methods I have found go through intermediate JSON files

Bakkes commented 1 year ago

Thats a good idea. I get exceptionally lazy with this stuff once i've developed it enough to fit my use case (hence why the readme hasnt really been updated since the initial commit). I'll write out some stuff which can people get started (soon™). I've also used the library to generate datasets for my own ML projects and that stuff is fairly general purpose, so I might add that code too so people can see how to interact with the library to get/construct useful data.

Since you've got fresh eyes on the replay format/project and I've worked with it for years: do you have any other things that you had wanted to be available/clear from the start when you dove into this apart from the stuff we've already discussed?

Also feel free to post any other questions in case they arise.

davechurchill commented 1 year ago

I had a bunch of stuff typed out here, but I realize now that a voice chat may be easier to discuss some things. I've had a pretty extensive history with game AI apis over the past decade or so (especially with Starcraft, since I run the AIIDE starcraft AI competition, you can check my repos) so I feel like I could give some good overall feedback.

If a voice chat is something you'd like to do you can contact me at dave.churchill@gmail.com, or if you don't want to, I could write a few paragraphs here instead.

davechurchill commented 1 year ago

One more quick data access question for you:

Given the above code, how can I access:

Sorry for all the questions, I know you're probably super busy

Bakkes commented 1 year ago

Sorry for all the questions, I know you're probably super busy

All good. It gives me an idea of what kind things I should probably add example code for somewhere. Not really sure if I want to go for a call, I'd just prefer a list of bullet points I could choose to work on whenever I want to. Reason for that is that I've (over the years) decided to heavily cut back on time I spend on free software (like bakkesmod) or open source projects since I've spent enough time on that for a lifetime, hence why it can take a while for me to reply as you can see 😛 .

I've uploaded the class I use to extract (useful) gamestates to a gist, but it might not be entirely suitable for your purposes. This one has info regarding boost amount & activity. Also looks like the replay parser might return incorrect values for angular velocities when compared to the game, so I'm dividing them by 100 here.

https://gist.github.com/Bakkes/ec79261e787923b47d83530ea6683849 Usage:

    auto replay = std::make_shared< CPPRP::ReplayFile>("test.reply");
    replay->Load();
    replay->DeserializeHeader();

    auto replayExtractor = ReplayTools::ReplayExtractor::ReplayStateExtractor(replay);
    if (replayExtractor.ShouldParse())
    {
        replay->Parse();
        auto& gamestates = replayExtractor.snapshots;
    }

Re: the now deleted question. That's the way to do it unfortunately (its done in the json serializer here: https://github.com/Bakkes/CPPRP/blob/master/CPPRPJSON/CPPRPJSON.h#L31). Although that code can probably be rewritten using a visitor 🤔

One last note in this comment: I'm not sure what the goal of your project is, but keep in mind that the data in replay files can be pretty terrible. For example, during my experiments in training a bot that tries to mimic state transitions from replays, a lot of the time actors simply don't update location/rotation information for multiple frames in a row so I had to write logic interpolate those. This can be the case for pretty much any actor.

davechurchill commented 1 year ago

Thank you for the help with the boost extract, that's pretty much the last thing I needed.

keep in mind that the data in replay files can be pretty terrible

Yeah I have experienced this to the point where I'm now comparing data that comes straight out of CPPRP to that extacted to JSON with rrrocket, and they both contains the same 'errors' that I have noticed in my visualizer. Sometimes actors seem to teleport for a frame or two, not get updated for a while, etc. What I've done is tried to identify and ignore those frames rather than try to interpolate to the level I've seen you do.

It's good that you said that though because I was starting to get to the point of wondering what I was doing wrong to get this weird data. But since you said it, and I'm getting the same errors with both methods, it's definitely the replay data itself.

davechurchill commented 1 year ago

I'd just prefer a list of bullet points I could choose to work on whenever I want to

I'll copy/paste the same feature request I left on the rrrocket github:


I am computer science professor / ai researcher, and a few colleagues / students have recently started looking to rocket league for data sets for machine learning. The output of rocket league replay parsers is quite confusing at first glance, and some considerable effort went into deciphering what various values means, etc.

What we are looking for is a JSON output that gives the following information:

The actual replay format makes this slightly annoying to extract, requiring me to go through and calculate the mapping from actorid to player names, find updated actors, and fill in frame data for which no updated actor data is found. If there was a way to output the data above directly from the program with a new flag, I think a lot of people would use it for this purpose. I do not know rust well enough to attempt this myself, and I have had some limited success with CPPRP in accomplishing this.

If you feel that this is a feature you may want to implement, I would be happy to provide a sample output JSON file with the type of data we are typically interested in for machine learning purposes.

davechurchill commented 1 year ago

Oops, one more piece of information I can't find in the replay file: Player Ladder Rankings

How might I go about extracting this for each player?

edit: I've been told this data is not stored in the replay files. rip

davechurchill commented 1 year ago
enum class GameState : uint8_t
{
    Uninitialized = 0,
    Countdown = 1,
    //PlayingNormal = 0b01,
    //PlayingOT     = 0b001,
    Playing = 2,
    PostGoal = 3,
    PostGame = 4,
};

Do you know how to get the PostGoal and PostGame information? I see how you're getting the CountDown information, but this does not let me know when the time post-goal where you can't move is happening. Been trying my best but can't figure this one out.

edit: FOUND IT!

CPPRP::TAGame::GameEvent_TA -> ReplicatedStateName appears to give the index into the names array containing the description of the current frame of the game.