feat(common): read the memory text the client sends at login

The login request's memoryStats is a run of short parts with no separators
(process working set and private bytes, memory load, physical memory, commit
limit, the 32-bit client's own address space, peaks). ParseMemoryStats splits
it into numbers; a text cut short keeps the parts before the cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 18:40:22 -05:00
parent 1e5660512e
commit b67e0ba9ba
5 changed files with 159 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
set(DCOMMON_SOURCES
"AMFDeserialize.cpp"
"ClientSysInfo.cpp"
"AmfSerialize.cpp"
"BinaryIO.cpp"
"dConfig.cpp"

61
dCommon/ClientSysInfo.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "ClientSysInfo.h"
#include <array>
#include <cctype>
#include <charconv>
namespace {
struct Reader {
std::string_view text;
size_t pos{};
void SkipSpace() {
while (pos < text.size() && std::isspace(static_cast<unsigned char>(text[pos]))) pos++;
}
bool Literal(std::string_view literal) {
SkipSpace();
if (text.substr(pos, literal.size()) != literal) return false;
pos += literal.size();
return true;
}
std::optional<int64_t> Number() {
SkipSpace();
int64_t value{};
const auto* begin = text.data() + pos;
const auto [end, error] = std::from_chars(begin, text.data() + text.size(), value);
if (error != std::errc{} || end == begin) return std::nullopt;
pos += static_cast<size_t>(end - begin);
return value;
}
};
}
ClientSysInfo::MemoryStats ClientSysInfo::ParseMemoryStats(const std::string_view text) {
MemoryStats stats;
// Each part: text before the number, text after it, and where the number goes
struct Part { std::string_view prefix; std::string_view suffix; std::optional<int64_t> MemoryStats::* field; };
const std::array<Part, 11> parts{ {
{ "", "p,", &MemoryStats::workingSetBytes },
{ "", "vbytes.", &MemoryStats::pagefileUsageBytes },
{ "", "n-use.", &MemoryStats::memoryLoadPercent },
{ "", "TKb-pmem.", &MemoryStats::totalPhysKb },
{ "", "FKb pmem.", &MemoryStats::availPhysKb },
{ "", "TKb pfile.", &MemoryStats::totalPageFileKb },
{ "", "FKb pfile.", &MemoryStats::availPageFileKb },
{ "", "TKbytes vmem.", &MemoryStats::totalVirtualKb },
{ "", "FKb vmem.", &MemoryStats::availVirtualKb },
{ "P", "p,", &MemoryStats::peakWorkingSetBytes },
{ "", "v.", &MemoryStats::peakPagefileUsageBytes },
} };
Reader reader{ text };
for (const auto& part : parts) {
if (!part.prefix.empty() && !reader.Literal(part.prefix)) return stats;
const auto value = reader.Number();
if (!value || !reader.Literal(part.suffix)) return stats;
stats.*part.field = *value;
}
stats.complete = true;
return stats;
}

39
dCommon/ClientSysInfo.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string_view>
/**
* The system description the client sends in its login request, as reported by the client. It comes from old Windows
* calls and describes what Windows (or Wine) tells a 32-bit program without a compatibility manifest, which is not
* necessarily the player's real hardware. docs/Dashboard.md (Client system info) says what each field means.
*/
namespace ClientSysInfo {
/**
* The login request's memoryStats text, split into numbers. The client writes these parts one after another with no
* separator (numbers are right-aligned to 7 characters):
* "<ws> p,<pf> vbytes." "<load> n-use." "<kb> TKb-pmem." "<kb> FKb pmem." "<kb> TKb pfile." "<kb> FKb pfile."
* "<kb> TKbytes vmem. \n" "<kb> FKb vmem." "P <peak ws> p,<peak pf> v."
* The client cuts the text at 255 characters, so the last parts can be missing; a part that doesn't match stops the
* parse and leaves it and everything after it empty.
*/
struct MemoryStats {
std::optional<int64_t> workingSetBytes; // the client process's working set at login (bytes)
std::optional<int64_t> pagefileUsageBytes; // the client process's private (commit) bytes at login
std::optional<int64_t> memoryLoadPercent; // how much of the system's memory Windows says is in use
std::optional<int64_t> totalPhysKb; // physical memory Windows reports (KB)
std::optional<int64_t> availPhysKb; // free physical memory at login (KB)
std::optional<int64_t> totalPageFileKb; // commit limit: physical memory plus page files (KB)
std::optional<int64_t> availPageFileKb; // free commit at login (KB)
std::optional<int64_t> totalVirtualKb; // the 32-bit client's own address space, not the system's (KB)
std::optional<int64_t> availVirtualKb; // free address space in the client (KB)
std::optional<int64_t> peakWorkingSetBytes; // the client process's peak working set (bytes)
std::optional<int64_t> peakPagefileUsageBytes; // the client process's peak private bytes
// Every part was read
bool complete{};
};
MemoryStats ParseMemoryStats(std::string_view text);
}

View File

@@ -1,5 +1,6 @@
set(DCOMMONTEST_SOURCES
"AMFDeserializeTests.cpp"
"ClientSysInfoTests.cpp"
"Amf3Tests.cpp"
"ToUnderlyingTests.cpp"
"InstanceMigrationTests.cpp"

View File

@@ -0,0 +1,57 @@
#include <gtest/gtest.h>
#include "ClientSysInfo.h"
using ClientSysInfo::ParseMemoryStats;
namespace {
// As the client writes it: each number right-aligned to 7 characters, no separators, " \n" after the vmem total
constexpr std::string_view FULL = "123456789 p,234567890 vbytes.45 n-use.16717048 TKb-pmem.8123456 FKb pmem.33434096 TKb pfile."
"20000000 FKb pfile.4194176 TKbytes vmem. \n3800000 FKb vmem.P 130000000 p,240000000 v.";
}
TEST(ClientSysInfoTests, ReadsEveryPart) {
const auto s = ParseMemoryStats(FULL);
EXPECT_TRUE(s.complete);
EXPECT_EQ(s.workingSetBytes, 123456789);
EXPECT_EQ(s.pagefileUsageBytes, 234567890);
EXPECT_EQ(s.memoryLoadPercent, 45);
EXPECT_EQ(s.totalPhysKb, 16717048);
EXPECT_EQ(s.availPhysKb, 8123456);
EXPECT_EQ(s.totalPageFileKb, 33434096);
EXPECT_EQ(s.availPageFileKb, 20000000);
EXPECT_EQ(s.totalVirtualKb, 4194176);
EXPECT_EQ(s.availVirtualKb, 3800000);
EXPECT_EQ(s.peakWorkingSetBytes, 130000000);
EXPECT_EQ(s.peakPagefileUsageBytes, 240000000);
}
TEST(ClientSysInfoTests, PaddedNumbers) {
const auto s = ParseMemoryStats(" 12345 p, 67890 vbytes.7 n-use.1048576 TKb-pmem. 52428 FKb pmem.");
EXPECT_FALSE(s.complete);
EXPECT_EQ(s.workingSetBytes, 12345);
EXPECT_EQ(s.pagefileUsageBytes, 67890);
EXPECT_EQ(s.memoryLoadPercent, 7);
EXPECT_EQ(s.totalPhysKb, 1048576);
EXPECT_EQ(s.availPhysKb, 52428);
EXPECT_FALSE(s.totalPageFileKb);
}
TEST(ClientSysInfoTests, CutTextKeepsWhatWasRead) {
// The client stops at 255 characters: a part cut in half is left out, with everything after it
const std::string cut(FULL.substr(0, FULL.find("FKb vmem.") + 3));
const auto s = ParseMemoryStats(cut);
EXPECT_FALSE(s.complete);
EXPECT_EQ(s.totalVirtualKb, 4194176);
EXPECT_FALSE(s.availVirtualKb);
EXPECT_FALSE(s.peakWorkingSetBytes);
}
TEST(ClientSysInfoTests, OtherTextReadsNothing) {
for (const auto* text : { "", "garbage", "12 q,34 vbytes.", "p, vbytes." }) {
const auto s = ParseMemoryStats(text);
EXPECT_FALSE(s.complete) << text;
EXPECT_FALSE(s.workingSetBytes) << text;
EXPECT_FALSE(s.totalPhysKb) << text;
}
}