Skip to content

Populate personal info riding records from race tracking#231

Open
TubaApollo wants to merge 44 commits into
Story-Of-Alicia:masterfrom
TubaApollo:fix/personal-info-basic-stats
Open

Populate personal info riding records from race tracking#231
TubaApollo wants to merge 44 commits into
Story-Of-Alicia:masterfrom
TubaApollo:fix/personal-info-basic-stats

Conversation

@TubaApollo

Copy link
Copy Markdown

Implements Riding records in My info Basic tab.
Related to #110

What works

  • Distance, top speed, longest gliding distance
  • Completion rate, average rank
  • Jump success rate, perfect jump success rate, consecutive perfect jumps
  • Best boost combo
  • Best magic defense combo
  • Win streaks (speed/magic, solo/team)
  • Magic item usage counts (stored but success rates need hit detection)

Known limitations

  • Magic attack success rates (magic ball, fire spirit, ice wall): usage is tracked but hit detection requires relay
    passthrough parsing (P2P data)
  • Avg chasing count --> needs relay passthrough parsing
  • Level progress --> needs level/XP table
  • Biggest carnival prize --> carnival system does not award prizes yet
  • Ceremony IDs needs achievement system

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
@rgnter rgnter self-requested a review March 13, 2026 07:50
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp
Comment thread src/server/race/RaceDirector.cpp
Comment thread src/server/race/RaceDirector.cpp
Comment thread src/server/race/RaceDirector.cpp Outdated
Comment thread src/server/race/RaceDirector.cpp
Comment thread src/server/race/RaceDirector.cpp
TubaApollo and others added 10 commits March 13, 2026 13:57
…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>
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/data/DataDefinitions.hpp
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.
@TubaApollo

Copy link
Copy Markdown
Author
{5F1378DA-74F1-479A-9030-F68091F3CC81} image

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.
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/LobbyMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/RanchMessageDefinitions.hpp Outdated
Comment thread include/libserver/network/command/proto/RanchMessageDefinitions.hpp Outdated
Comment thread src/libserver/network/command/proto/RanchMessageDefinitions.cpp
Comment thread src/libserver/network/command/proto/RanchMessageDefinitions.cpp
Comment thread src/server/race/RaceDirector.cpp Outdated
Comment on lines +628 to +629
_serverInstance.GetDataDirector().GetHorse(
character.mountUid())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_serverInstance.GetDataDirector().GetHorse(
character.mountUid())
_serverInstance.GetDataDirector().GetHorse(character.mountUid())

Comment thread src/server/race/RaceDirector.cpp
if (emblemId > 0 && emblemId <= 35)
{
const auto& lobbyContext = GetClientContext(clientId);
_serverInstance.GetRanchDirector().SendEmblemNotify(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@TubaApollo TubaApollo Mar 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's how notify works, but you're sending it when player is logging in (and only to them) for some reason.

@TubaApollo TubaApollo Mar 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rgnter rgnter Mar 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

TubaApollo and others added 14 commits March 14, 2026 22:33
…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%.
Comment on lines +1797 to +1861
// 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
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the lifetime stats of the player, not the current horse.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to character stats now

Comment thread src/server/ranch/RanchDirector.cpp Outdated
Comment on lines +636 to +671
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.
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't send the notify twice, once should be enough.
Also we should prevent self broadcasting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants