Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3f785ff
feat: populate personal info basic stats from mount data
TubaApollo Mar 9, 2026
b31366e
refactor: rename personal info fields from positional to semantic names
TubaApollo Mar 9, 2026
894aa5d
feat: track race and jump statistics persistently in mount data
TubaApollo Mar 10, 2026
00c91f6
feat: track distance, speed and glide stats during races
TubaApollo Mar 12, 2026
d240b66
fix: add mount guards, improve glide tracking and use named magic con…
TubaApollo Mar 12, 2026
eaf8d25
feat: track boost combo and win streaks in race statistics
TubaApollo Mar 12, 2026
de9dd59
fix: use consistent {} initialization and add team win streak TODO
TubaApollo Mar 12, 2026
e2653e0
style: apply clang-format to new code
TubaApollo Mar 12, 2026
434577f
Update include/libserver/network/command/proto/LobbyMessageDefinition…
TubaApollo Mar 13, 2026
e69fbd1
Update include/libserver/network/command/proto/LobbyMessageDefinition…
TubaApollo Mar 13, 2026
2707c4f
Update include/libserver/network/command/proto/LobbyMessageDefinition…
TubaApollo Mar 13, 2026
3fc368d
Update src/server/race/RaceDirector.cpp
TubaApollo Mar 13, 2026
f4a684b
Update src/server/race/RaceDirector.cpp
TubaApollo Mar 13, 2026
857b152
Update src/server/race/RaceDirector.cpp
TubaApollo Mar 13, 2026
72044f8
Update src/server/race/RaceDirector.cpp
TubaApollo Mar 13, 2026
d9ea435
Rename ceremonyId to ceremonySlot, legacyGradeString to gradeTitle
TubaApollo Mar 13, 2026
7b6f637
Fix magic item tracking, add Summon for fire spirit
TubaApollo Mar 13, 2026
258fc94
Fix team win stats to not reset on loss
TubaApollo Mar 13, 2026
203bb98
Flatten mount stats with early returns and named variables
TubaApollo Mar 13, 2026
8565d6f
Remove duplicate comment on padding field
TubaApollo Mar 13, 2026
b4eff96
Rename ceremonySlot to ceremonyAchievementSlot1-3
TubaApollo Mar 13, 2026
18e05bb
Replace em dashes with hyphens in comments
TubaApollo Mar 13, 2026
60d2fca
Add per-field doxygen comments to mount info counters
TubaApollo Mar 13, 2026
5789317
Fix locked ranch bypass, restore missing early return
TubaApollo Mar 13, 2026
87c0ad4
Implement emblem display and selection (SetKeyEmblem 0x254-0x257)
TubaApollo Mar 14, 2026
9c901e7
Rename gradeTitle to unk_gradeString, document client ignores it
TubaApollo Mar 14, 2026
412c83b
Fix unk_gradeString comment: EmblemTable -> AchievementGrade
TubaApollo Mar 14, 2026
1497429
Update include/libserver/network/command/proto/LobbyMessageDefinition…
TubaApollo Mar 14, 2026
73918dd
Update include/libserver/network/command/proto/LobbyMessageDefinition…
TubaApollo Mar 14, 2026
8e15962
Fix ternary and lambda indentation in RaceDirector
TubaApollo Mar 14, 2026
1fe1171
Update include/libserver/network/command/proto/RanchMessageDefinition…
TubaApollo Mar 14, 2026
f6b1736
Update include/libserver/network/command/proto/RanchMessageDefinition…
TubaApollo Mar 14, 2026
49ad848
Update src/libserver/network/command/proto/RanchMessageDefinitions.cpp
TubaApollo Mar 14, 2026
acf5cc9
Update include/libserver/network/command/proto/RanchMessageDefinition…
TubaApollo Mar 14, 2026
d03b621
Update include/libserver/network/command/proto/RanchMessageDefinition…
TubaApollo Mar 14, 2026
43ba547
Fix GetHorse call alignment in RaceDirector
TubaApollo Mar 14, 2026
eef5b98
Update src/libserver/network/command/proto/RanchMessageDefinitions.cpp
TubaApollo Mar 14, 2026
90b4c14
Add BroadcastEmblemNotify, use it in HandleSetKeyEmblem
TubaApollo Mar 14, 2026
92f8624
Fix overhead emblem not refreshing on emblem change
TubaApollo Mar 19, 2026
9b36c46
Fix completion rate counting DNF racers as finished
TubaApollo Mar 19, 2026
912e137
Aggregate personal info stats across all horses, fix emblem broadcast
TubaApollo Mar 19, 2026
5e12037
Move personal info stats from per-horse to character lifetime
TubaApollo Mar 20, 2026
afc12c1
Remove stale TODO - per-horse stats view already implemented
TubaApollo Mar 20, 2026
f2d6ecc
Resolve merge conflict in RaceDirector.cpp
TubaApollo Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions include/libserver/data/DataDefinitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,24 @@ struct Horse
dao::Field<uint32_t> participated{};
dao::Field<uint32_t> cumulativePrize{};
dao::Field<uint32_t> biggestPrize{};

// Race tracking counters
dao::Field<uint32_t> totalRaces{};
dao::Field<uint32_t> totalFinished{};
dao::Field<uint32_t> cumulativeRank{};

// Jump tracking counters
dao::Field<uint32_t> totalJumps{};
dao::Field<uint32_t> successfulJumps{};
dao::Field<uint32_t> perfectJumps{};
dao::Field<uint32_t> bestJumpCombo{};
dao::Field<uint32_t> bestMagicDefenseCombo{};

// Magic item usage counters (for success rate calculation).
// TODO: Hit counters need relay passthrough parsing to detect hits.
dao::Field<uint32_t> magicBallUses{};
dao::Field<uint32_t> iceWallUses{};
dao::Field<uint32_t> fireSpiritUses{};
Comment thread
TubaApollo marked this conversation as resolved.
} mountInfo{};
};

Expand Down
38 changes: 28 additions & 10 deletions include/libserver/network/command/proto/LobbyMessageDefinitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1792,25 +1792,43 @@ struct AcCmdLCPersonalInfo
uint16_t magicTeamWinCombo{};
float averageRank{};
float completionRate{};
float member12{};
//! Average chasing count.
//! Displayed as a percentage represented by a floating point in an interval <x, y>.
float averageChasingCount{};
uint32_t highestCarnivalPrize{};
uint16_t member14{};
uint16_t member15{};
uint16_t member16{};
//! Achievement ceremony slot 0 (icon_ceremony1 in UI).
//! References a ceremony type from the AchievementCeremony table.
uint16_t ceremonySlot0{};
//! Achievement ceremony slot 1 (icon_ceremony2 in UI).
//! References a ceremony type from the AchievementCeremony table.
uint16_t ceremonySlot1{};
//! Achievement ceremony slot 2 (icon_ceremony3 in UI).
//! References a ceremony type from the AchievementCeremony table.
uint16_t ceremonySlot2{};
std::string introduction{};
uint32_t level{60};
//! Level progress as dictated by LevelInfo table in libconfig
uint32_t levelProgress{};
std::string member20{};
//! Achievement grade title displayed in profile (txt_grade in UI).
std::string gradeTitle{};
uint16_t perfectBoostCombo{};
uint16_t perfectJumpCombo{};
uint16_t magicDefenseCombo{};
float member24{};
float member25{};
float member26{};
//! Magic ball (Dark fire and Fireball) attack success rate.
//! Displayed as a percentage represented by a floating point in an interval <0, 1>.
float magicBallAttackSuccessRate{};
//! Fire spirit transfer success rate.
//! Displayed as a percentage represented by a floating point in an interval <0, 1>.
float fireSpiritTransferSuccessRate{};
//! Ice wall attack success rate.
//! Displayed as a percentage represented by a floating point in an interval <0, 1>.
float iceWallAttackSuccessRate{};
std::string guildName{};
uint8_t member28{};
uint8_t member29{};
//! Not accessed by client UI handler. Likely padding.
Comment thread
TubaApollo marked this conversation as resolved.
Outdated
//! Not used by client.
uint8_t padding28{};
//! Not used by client.
uint8_t padding29{};
Comment thread
TubaApollo marked this conversation as resolved.

static void Write(const Basic& command, SinkStream& stream);
static void Read(Basic& command, SourceStream& stream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1493,17 +1493,17 @@ struct AcCmdUserRaceUpdatePos
//! Character oid
uint16_t oid{};
//! Position
std::array<float, 3> member2{};
std::array<float, 3> position{};
//! Rotation
std::array<float, 3> member3{};
//! Speed
float member4{};
std::array<float, 3> rotation{};
//! Speed in km/h.
float speed{};
//! 1 = In the air
uint16_t member5{};
uint16_t airborne{};
//! Race track progress
float member6{};
float trackProgress{};
//! Ticks since connected to race director?
uint32_t member7{};
uint32_t ticksSinceConnect{};

static Command GetCommand()
{
Expand Down
23 changes: 23 additions & 0 deletions include/server/tracker/RaceTracker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,29 @@ class RaceTracker
bool hotRodded{};
bool critChance{};
bool gaugeBuff{};

//! Session boost combo (consecutive boosts without combo break).
uint32_t boostComboValue{};
//! Session magic defense combo (consecutive blocks).
uint32_t magicDefenseComboValue{};
//! Session magic item usage counts by item ID.
uint32_t magicBallUses{};
uint32_t iceWallUses{};
uint32_t fireSpiritUses{};

//! Session distance/speed/glide tracking.
std::optional<std::array<float, 3>> previousPosition{};
//! Accumulated distance in metres during this race.
float sessionDistance{};
//! Maximum speed observed during this race (km/h from protocol).
//! Persisted as km/h * 10 (e.g. 1028 = 102.8 km/h).
float sessionMaxSpeed{};
//! Current glide distance accumulator (reset on landing).
float currentGlideDistance{};
//! Longest glide distance observed during this race.
float sessionLongestGlide{};
//! Whether the racer was airborne in the previous update.
bool previousAirborne{};
};

//! An item
Expand Down
24 changes: 23 additions & 1 deletion src/libserver/data/file/FileDataSource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,18 @@ void server::FileDataSource::RetrieveHorse(data::Uid uid, data::Horse& horse)
.longestGlideDistance = mountInfo["longestGlideDistance"].get<uint32_t>(),
.participated = mountInfo["participated"].get<uint32_t>(),
.cumulativePrize = mountInfo["cumulativePrize"].get<uint32_t>(),
.biggestPrize = mountInfo["biggestPrize"].get<uint32_t>()};
.biggestPrize = mountInfo["biggestPrize"].get<uint32_t>(),
.totalRaces = mountInfo.value("totalRaces", 0u),
.totalFinished = mountInfo.value("totalFinished", 0u),
.cumulativeRank = mountInfo.value("cumulativeRank", 0u),
.totalJumps = mountInfo.value("totalJumps", 0u),
.successfulJumps = mountInfo.value("successfulJumps", 0u),
.perfectJumps = mountInfo.value("perfectJumps", 0u),
.bestJumpCombo = mountInfo.value("bestJumpCombo", 0u),
.bestMagicDefenseCombo = mountInfo.value("bestMagicDefenseCombo", 0u),
.magicBallUses = mountInfo.value("magicBallUses", 0u),
.iceWallUses = mountInfo.value("iceWallUses", 0u),
.fireSpiritUses = mountInfo.value("fireSpiritUses", 0u)};
}

void server::FileDataSource::StoreHorse(data::Uid uid, const data::Horse& horse)
Expand Down Expand Up @@ -715,6 +726,17 @@ void server::FileDataSource::StoreHorse(data::Uid uid, const data::Horse& horse)
mountInfo["participated"] = horse.mountInfo.participated();
mountInfo["cumulativePrize"] = horse.mountInfo.cumulativePrize();
mountInfo["biggestPrize"] = horse.mountInfo.biggestPrize();
mountInfo["totalRaces"] = horse.mountInfo.totalRaces();
mountInfo["totalFinished"] = horse.mountInfo.totalFinished();
mountInfo["cumulativeRank"] = horse.mountInfo.cumulativeRank();
mountInfo["totalJumps"] = horse.mountInfo.totalJumps();
mountInfo["successfulJumps"] = horse.mountInfo.successfulJumps();
mountInfo["perfectJumps"] = horse.mountInfo.perfectJumps();
mountInfo["bestJumpCombo"] = horse.mountInfo.bestJumpCombo();
mountInfo["bestMagicDefenseCombo"] = horse.mountInfo.bestMagicDefenseCombo();
mountInfo["magicBallUses"] = horse.mountInfo.magicBallUses();
mountInfo["iceWallUses"] = horse.mountInfo.iceWallUses();
mountInfo["fireSpiritUses"] = horse.mountInfo.fireSpiritUses();
json["mountInfo"] = mountInfo;
dataFile << json.dump(2);
}
Expand Down
20 changes: 10 additions & 10 deletions src/libserver/network/command/proto/LobbyMessageDefinitions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1153,24 +1153,24 @@ void AcCmdLCPersonalInfo::Basic::Write(
.Write(command.magicTeamWinCombo)
.Write(command.averageRank)
.Write(command.completionRate)
.Write(command.member12)
.Write(command.averageChasingCount)
.Write(command.highestCarnivalPrize)
.Write(command.member14)
.Write(command.member15)
.Write(command.member16)
.Write(command.ceremonySlot0)
.Write(command.ceremonySlot1)
.Write(command.ceremonySlot2)
.Write(command.introduction)
.Write(command.level)
.Write(command.levelProgress)
.Write(command.member20)
.Write(command.gradeTitle)
.Write(command.perfectBoostCombo)
.Write(command.perfectJumpCombo)
.Write(command.magicDefenseCombo)
.Write(command.member24)
.Write(command.member25)
.Write(command.member26)
.Write(command.magicBallAttackSuccessRate)
.Write(command.fireSpiritTransferSuccessRate)
.Write(command.iceWallAttackSuccessRate)
.Write(command.guildName)
.Write(command.member28)
.Write(command.member29);
.Write(command.padding28)
.Write(command.padding29);
}

void AcCmdLCPersonalInfo::Basic::Read(
Expand Down
12 changes: 6 additions & 6 deletions src/libserver/network/command/proto/RaceMessageDefinitions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1077,20 +1077,20 @@ void AcCmdUserRaceUpdatePos::Read(
{
stream.Read(command.oid);

for (auto& element : command.member2)
for (auto& element : command.position)
{
stream.Read(element);
}

for (auto& element : command.member3)
for (auto& element : command.rotation)
{
stream.Read(element);
}

stream.Read(command.member4)
.Read(command.member5)
.Read(command.member6)
.Read(command.member7);
stream.Read(command.speed)
.Read(command.airborne)
.Read(command.trackProgress)
.Read(command.ticksSinceConnect);
}

void AcCmdRCRoomCountdown::Write(
Expand Down
69 changes: 68 additions & 1 deletion src/server/lobby/LobbyNetworkHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,7 @@ void LobbyNetworkHandler::HandleRequestPersonalInfo(
{
case protocol::AcCmdCLRequestPersonalInfo::Type::Basic:
{
// Guild name
const auto& guildRecord = _serverInstance.GetDataDirector().GetGuild(
character.guildUid());
if (guildRecord.IsAvailable())
Expand All @@ -1790,9 +1791,75 @@ void LobbyNetworkHandler::HandleRequestPersonalInfo(
});
}

// Character info
response.basic.introduction = character.introduction();
response.basic.level = character.level();
// TODO: implement other stats

// 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

break;
}
case protocol::AcCmdCLRequestPersonalInfo::Type::Courses:
Expand Down
Loading