feat(stats): zone statistics for quickbuilds and achievements reach the client

Live sent the player ModifyPlayerZoneStatistic(set false, value 1, the
current map) for "QuickBuildsCompleted" (227) after a quickbuild's
EnableRebuild and for "AchievementsCompleted" (186) at the end of an
achievement's completion. DLU counted both server side (they are saved
and shown at the next load) but never told the client, so the zone
summary only caught up after a zone change. The client counts
enemies, coins and bricks itself and sends those to the server.

Check: complete a quickbuild and an achievement, then open the zone
summary (passport / map zone stats) without changing zones: both
counts went up by one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:16:39 -05:00
parent 08053a1b01
commit 73fbfa840a
5 changed files with 59 additions and 0 deletions

View File

@@ -5,10 +5,13 @@
#include "CharacterComponent.h"
#include "CDClientDatabase.h"
#include "Entity.h"
#include "PlayerMessages.h"
#include "dZoneManager.h"
#include "QuickBuildComponent.h"
#include "QuickBuildMessages.h"
#include "eQuickBuildState.h"
#include <algorithm>
#include <memory>
#include <gtest/gtest.h>
@@ -37,6 +40,7 @@ protected:
void SetUp() override {
SetUpDependencies();
ConnectEmptyCDClient();
Game::zoneManager->LoadZone(LWOZONEID(1101, 0, 0)); // the zone of the live samples below
auto playerInfo = info;
playerInfo.lot = 1;
player = std::make_unique<Entity>(0x1000000000000001LL, playerInfo);
@@ -88,3 +92,34 @@ TEST_F(QuickBuildCompleteTests, SuccessHasNoDuration) {
EXPECT_EQ(enables[0].fDuration, 0.0f);
EXPECT_EQ(enables[0].user, player->GetObjectID());
}
// Live sends the builder one more QuickBuildsCompleted for this zone after the EnableRebuild. Bytes: a live
// ModifyPlayerZoneStatistic from zone 1101 with the player ID replaced.
TEST_F(QuickBuildCompleteTests, ZoneStatisticFollowsEnableRebuild) {
const auto sent = Build();
const auto ids = SentGameMessageIds(sent);
const auto enable = std::find(ids.begin(), ids.end(), MessageType::Game::ENABLE_REBUILD);
const auto statistic = std::find(ids.begin(), ids.end(), MessageType::Game::MODIFY_PLAYER_ZONE_STATISTIC);
ASSERT_NE(statistic, ids.end());
EXPECT_LT(enable, statistic);
std::vector<CapturedPacket> statistics;
for (const auto& packet : sent) {
if (!SentGameMessages<GameMessages::ModifyPlayerZoneStatistic>({ packet }).empty()) statistics.push_back(packet);
}
ASSERT_EQ(statistics.size(), 1u);
EXPECT_EQ(statistics[0].sysAddr, ClientAddress());
EXPECT_FALSE(statistics[0].broadcast);
EXPECT_PACKET_EQ(FromHex(
"53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 16 04 0a 00 00 00 28 80 3a 80 34 80 31 80 35 80 21 00 3a 80 34 80 "
"36 00 32 00 39 80 21 80 37 80 36 80 38 00 36 00 32 80 3a 00 32 80 32 00 40 40 00 00 29 a0 80", 547), FromCapture(statistics[0]));
}
// The same message for a completed achievement (sent from Mission::Complete).
TEST_F(QuickBuildCompleteTests, AchievementZoneStatisticMatchesLive) {
const auto sent = Capture([&] { player->GetComponent<CharacterComponent>()->SendZoneStatisticIncrement(u"AchievementsCompleted"); });
ASSERT_EQ(sent.size(), 1u);
EXPECT_PACKET_EQ(FromHex(
"53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 16 04 0a 80 00 00 20 80 31 80 34 00 34 80 32 80 3b 00 32 80 36 80 "
"32 80 37 00 3a 00 39 80 21 80 37 80 36 80 38 00 36 00 32 80 3a 00 32 80 32 00 40 40 00 00 29 a0 80", 563), FromCapture(sent[0]));
}