Populate personal info riding records from race tracking#231
Populate personal info riding records from race tracking#231TubaApollo wants to merge 44 commits into
Conversation
Rename unknown memberXX fields in AcCmdLCPersonalInfo::Basic based on client RE (Ghidra analysis of FUN_008a8ef0 UI update function). Populate mount statistics (distance, speed, wins, combos, carnival prize) in HandleRequestPersonalInfo from active mount's MountInfo. Computed stats (jump rates, avg rank, completion rate) left as 0 until race result tracking is implemented. Related to Story-Of-Alicia#115
Extracted UI labels from res.pak XML templates (myinfo_dialog.xml) and matched them to packet fields via Ghidra RE + Y-position pairing: - record7Stat → averageChasingCount (체이싱 평균 횟수) - record18Rate → magicBallAttackSuccessRate (마법구 공격 성공율) - record19Rate → fireSpiritTransferSuccessRate (화염의 정령 옮기기 성공율) - record17Rate → iceWallAttackSuccessRate (빙벽 공격 성공율)
Add cumulative counters to MountInfo, update during hurdle clear events and race completion, compute derived stats for personal info.
- Rename AcCmdUserRaceUpdatePos members to semantic names (position, rotation, speed, airborne, trackProgress) - Rename personal info fields from RE findings: uiState14/15/16 -> ceremonyId1/2/3 (achievement ceremony slots) record20String -> legacyGradeString (unused by client) flag28/29 -> padding28/29 (not accessed by UI handler) - Track distance, max speed and longest glide per race session in RaceTracker::Racer, persisted to MountInfo at race end - Speed and glide stored as value * 10 for client display format Not yet implemented (each needs a separate feature branch): - magicBallAttackSuccessRate, fireSpiritTransferSuccessRate, iceWallAttackSuccessRate: usage counts exist in mountInfo but hit detection requires relay message parsing (P2P passthrough) - averageChasingCount: "chasing" is a MissionType in the NPC quest system (RcUILogicMissionChase). Needs quest/mission system (see origin/feature/quests) - levelProgress: needs character XP field, LevelInfo threshold table from libconfig_c.dat (100 levels), and XP earning logic - ceremonyId1/2/3: achievement ceremony slot IDs for profile icons, needs achievement system
…stants - Add missing mountUid != InvalidUid guards in jump tracking - Fix glide tracking to count takeoff tick and handle teleport resets - Replace magic item ID literals with named constexpr constants - Document sessionMaxSpeed unit and defense combo P2P relay flow
- Track consecutive boosts in HandleRequestSpur (reset on comboBreak) - Flush best boost combo to boostsInARow at race end - Update win streak counters based on game mode and team mode
- Replace {0u} with {} on dao::Field members to match existing style
- Document that team mode win streaks use individual rank as placeholder
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Add AcCmdCRSetKeyEmblemNotify, AcCmdCRSetKeyEmblem request/OK/cancel protocol messages. Populate emblem list with IDs 1-35 from EmblemInfo table. Route emblem notify through RanchDirector for cross-server delivery when viewing personal info in the lobby.
The client parses this field but never reads it back. The txt_grade label in the profile UI is populated from a client-side EmblemTable lookup instead.
| _serverInstance.GetDataDirector().GetHorse( | ||
| character.mountUid()) |
There was a problem hiding this comment.
| _serverInstance.GetDataDirector().GetHorse( | |
| character.mountUid()) | |
| _serverInstance.GetDataDirector().GetHorse(character.mountUid()) |
| if (emblemId > 0 && emblemId <= 35) | ||
| { | ||
| const auto& lobbyContext = GetClientContext(clientId); | ||
| _serverInstance.GetRanchDirector().SendEmblemNotify( |
There was a problem hiding this comment.
Why does this need to happen here? AcCmdCRSetKeyEmblemNotify is only supposed to send notification about picked emblems after user explicitly changes them. Why do you force it here?
There was a problem hiding this comment.
Without this, when player A views player B's profile after B changed their emblem, the profile dialog shows the new emblem but the emblem above B's head still shows the old one. The notify syncs the overhead emblem display when another player opens your profile.
There was a problem hiding this comment.
Yeah that's how notify works, but you're sending it when player is logging in (and only to them) for some reason.
There was a problem hiding this comment.
Okay I rechecked. I think there are two options:
Either accept the mismatch between profile dialog and overhead emblem. The profile dialog will show the correct emblem, but the overhead display stays stale when a user changed it.
Edit: Or keep the notify. Tried to recheck with xdbg:
- BroadcastEmblemNotify in HandleSetKeyEmblem does update the client's local character cache, but doesn't trigger a visual refresh of the overhead emblem
- Opening the profile alone also doesn't refresh the overhead
- The notify sent while the profile dialog is open is what triggers the actual visual update of the overhead emblem
If we keep it, I'll add a guard so it only fires when viewing another player's profile, not on login.
Since it's not completely clear why this only works with the profile dialog open, it might be better to just remove it and accept the mismatch.
There was a problem hiding this comment.
HandleRequestPersonalInfo is invoked when someone clicks on someone's profile. If overhead emblem and emblem in profile are mismatched we're missing something. Can you please record the behavior and send it in #server-development on discord?
We simply cannot be sending this here, it is incorrect.
There was a problem hiding this comment.
I think I finally found out why.
When AcCmdCRSetKeyEmblemNotify fires, client dispatches gMsgUIChangeKeyEmblem. Two subscribers react with the same priority, the Overhead Handler and the Cache Updater. But the Overhead Handler fires BEFORE the Cache Updater. (confirmed with xdbg) Thats why the Overhead reads the old value.
I fixed it now by sending the same notify twice. It's not perfect but I don't think there is any better way to do it?
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
…s.hpp Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
Co-authored-by: Maroš Prejsa <maros.prejsa1@gmail.com>
The client's overhead emblem handler reads from a character cache that is updated by a separate signal subscriber firing after it. Send the notify twice so the second dispatch reads the correct value. Remove the workaround that sent EmblemNotify from HandleRequestPersonalInfo.
Only count racers who crossed the finish line (courseTime >= 0) toward totalFinished and cumulativeRank. Previously any non-disconnected racer was counted, so DNF players inflated the completion rate to 100%.
| // Mount statistics from active mount | ||
| if (character.mountUid() == data::InvalidUid) | ||
| return; | ||
|
|
||
| const auto mountRecord = _serverInstance.GetDataDirector().GetHorse( | ||
| character.mountUid()); | ||
|
|
||
| if (not mountRecord) | ||
| return; | ||
|
|
||
| mountRecord.Immutable([&response](const data::Horse& horse) | ||
| { | ||
| response.basic.distanceTravelled = horse.mountInfo.totalDistance(); | ||
| response.basic.topSpeed = horse.mountInfo.topSpeed(); | ||
| response.basic.longestGlidingDistance = horse.mountInfo.longestGlideDistance(); | ||
| response.basic.speedSingleWinCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.winsSpeedSingle()); | ||
| response.basic.speedTeamWinCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.winsSpeedTeam()); | ||
| response.basic.magicSingleWinCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.winsMagicSingle()); | ||
| response.basic.magicTeamWinCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.winsMagicTeam()); | ||
| response.basic.highestCarnivalPrize = horse.mountInfo.biggestPrize(); | ||
| response.basic.perfectBoostCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.boostsInARow()); | ||
|
|
||
| // Computed race stats from persistent counters | ||
| if (horse.mountInfo.totalJumps() > 0) | ||
| { | ||
| const float successfulJumps = static_cast<float>(horse.mountInfo.successfulJumps()); | ||
| const float perfectJumps = static_cast<float>(horse.mountInfo.perfectJumps()); | ||
| const float totalJumps = static_cast<float>(horse.mountInfo.totalJumps()); | ||
|
|
||
| response.basic.jumpSuccessRate = successfulJumps / totalJumps; | ||
| response.basic.perfectJumpSuccessRate = perfectJumps / totalJumps; | ||
| } | ||
|
|
||
| const float totalFinished = static_cast<float>(horse.mountInfo.totalFinished()); | ||
|
|
||
| if (horse.mountInfo.totalFinished() > 0) | ||
| { | ||
| const float cumulativeRank = static_cast<float>(horse.mountInfo.cumulativeRank()); | ||
| response.basic.averageRank = cumulativeRank / totalFinished; | ||
| } | ||
|
|
||
| if (horse.mountInfo.totalRaces() > 0) | ||
| { | ||
| const float totalRaces = static_cast<float>(horse.mountInfo.totalRaces()); | ||
| response.basic.completionRate = totalFinished / totalRaces; | ||
| } | ||
|
|
||
| response.basic.perfectJumpCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.bestJumpCombo()); | ||
| response.basic.magicDefenseCombo = static_cast<uint16_t>( | ||
| horse.mountInfo.bestMagicDefenseCombo()); | ||
|
|
||
| // TODO: Magic attack SUCCESS RATES require relay passthrough | ||
| // parsing to detect hits (P2P data). Usage counts are tracked | ||
| // in mountInfo but hit counts need feature/reverse-relay-data. | ||
| // Fields: magicBallAttackSuccessRate, fireSpiritTransferSuccessRate, | ||
| // iceWallAttackSuccessRate, averageChasingCount | ||
| // TODO: levelProgress needs res.pak level/XP table extraction | ||
| }); | ||
|
|
There was a problem hiding this comment.
This should be the lifetime stats of the player, not the current horse.
There was a problem hiding this comment.
Changed to character stats now
| void RanchDirector::BroadcastEmblemNotify( | ||
| const data::Uid characterUid, | ||
| const uint16_t emblemId) | ||
| { | ||
| try | ||
| { | ||
| const auto& clientContext = GetClientContextByCharacterUid(characterUid); | ||
|
|
||
| // Send twice - the client's overhead handler reads from a cache | ||
| // that is updated by a later subscriber in the signal chain. | ||
| protocol::AcCmdCRSetKeyEmblemNotify notify{ | ||
| .characterUid = static_cast<uint32_t>(characterUid), | ||
| .emblemId = emblemId}; | ||
|
|
||
| for (const ClientId ranchClientId : _ranches[clientContext.visitingRancherUid].clients) | ||
| { | ||
| _commandServer.QueueCommand<decltype(notify)>( | ||
| ranchClientId, | ||
| [notify]() | ||
| { | ||
| return notify; | ||
| }); | ||
| _commandServer.QueueCommand<decltype(notify)>( | ||
| ranchClientId, | ||
| [notify]() | ||
| { | ||
| return notify; | ||
| }); | ||
| } | ||
| } | ||
| catch (const std::exception&) | ||
| { | ||
| // Empty. | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
We shouldn't send the notify twice, once should be enough.
Also we should prevent self broadcasting.
There was a problem hiding this comment.
Self-broadcast is fixed. Don't really know how to proceed with the notify, I can remove it but then it would need a client patch to work.
Personal info stats now iterate all horses (stable + active mount) instead of reading only the active mount. Sums cumulative counters, takes max for records/combos. Remove unused SendEmblemNotify (replaced by BroadcastEmblemNotify). Add self-broadcast prevention to BroadcastEmblemNotify.
Add Character::RidingStats for lifetime stats that persist across horse changes. Race stats are now written to both the horse (per-mount view) and the character (personal info). Personal info reads directly from character.ridingStats instead of aggregating across horses.
Keep magic item usage tracking and add magicSlotInfo lookup from master.


Implements Riding records in My info Basic tab.
Related to #110
What works
Known limitations
passthrough parsing (P2P data)