15 namespace fsys = std::filesystem;
21 std::vector<std::string_view> splitFields(std::string_view line) {
22 std::vector<std::string_view> fields;
24 const auto bar = line.find(
'|');
25 if (bar == std::string_view::npos)
break;
26 fields.push_back(line.substr(0, bar));
27 line.remove_prefix(bar + 1);
29 fields.push_back(line);
37 std::string_view columnName(std::string_view headerField) {
38 return headerField.substr(0, headerField.find(
'!'));
45 std::optional<std::vector<std::string>> readLines(
const fsys::path& path) {
46 std::ifstream file{path};
47 if (!file)
return std::nullopt;
49 std::vector<std::string> lines;
50 for (std::string line; std::getline(file, line);) {
51 if (line.ends_with(
'\r')) line.pop_back();
52 if (!line.empty()) lines.push_back(std::move(line));
61 std::optional<std::string> readFlavor(
const fsys::path& clientPath) {
62 const auto lines = readLines(clientPath /
".flavor.info");
63 if (!lines)
return std::nullopt;
64 for (
const std::string& line : *lines)
65 if (!line.starts_with(
"##"))
return line;
74 std::optional<fsys::path> findBuildInfo(
const fsys::path& clientPath) {
76 for (
const fsys::path& candidate : {clientPath /
".build.info", clientPath.parent_path() /
".build.info"})
77 if (fsys::is_regular_file(candidate, ec))
return candidate;
85 std::optional<ClientVersion> parseVersion(std::string_view text) {
86 std::array<std::uint32_t, 4> parts{};
87 for (std::uint32_t& part : parts) {
88 const auto dot = text.find(
'.');
89 const std::string_view field = text.substr(0, dot);
90 const auto [end, ec] = std::from_chars(field.data(), field.data() + field.size(), part);
91 if (ec != std::errc{} || end != field.data() + field.size())
return std::nullopt;
92 text = dot == std::string_view::npos ? std::string_view{} : text.substr(dot + 1);
95 static_cast<std::uint16_t
>(parts[0]),
96 static_cast<std::uint16_t
>(parts[1]),
97 static_cast<std::uint16_t
>(parts[2]),
115 struct BuildInfoRow {
124 std::vector<BuildInfoRow> parseBuildInfo(
const std::vector<std::string>& lines) {
125 if (lines.empty())
return {};
127 const auto header = splitFields(lines.front());
128 const auto column = [&](std::string_view name) -> std::size_t {
129 const auto found = std::ranges::find_if(header, [&](std::string_view field) {
130 return columnName(field) == name;
132 return static_cast<std::size_t
>(found - header.begin());
134 const std::size_t productAt = column(
"Product");
135 const std::size_t versionAt = column(
"Version");
136 const std::size_t activeAt = column(
"Active");
138 std::vector<BuildInfoRow> rows;
139 for (
const std::string& line : lines | std::views::drop(1)) {
140 const auto fields = splitFields(line);
141 if (productAt >= fields.size() || versionAt >= fields.size())
continue;
143 .product = std::string{fields[productAt]},
144 .version = std::string{fields[versionAt]},
145 .active = activeAt < fields.size() && fields[activeAt] !=
"0"
153 const auto infoPath = findBuildInfo(clientPath);
157 "no .build.info in '{}' or its parent — not a CASC installation "
158 "(MPQ-era clients and bare repacks record no version; construct "
159 "the ClientVersion directly)", clientPath.string()));
161 const auto lines = readLines(*infoPath);
165 auto rows = parseBuildInfo(*lines);
166 std::erase_if(rows, [](
const BuildInfoRow& row) {
return !row.active; });
172 const auto flavor = readFlavor(clientPath);
174 const auto match = std::ranges::find(rows, *flavor, &BuildInfoRow::product);
175 if (match == rows.end())
177 (clientPath /
".flavor.info").
string(), *flavor,
178 infoPath->string()));
181 else if (rows.size() > 1) {
182 std::string products;
183 for (
const BuildInfoRow& row : rows) products += (products.empty() ?
"" :
", ") + row.product;
185 "'{}' lists several products ({}) and '{}' has no .flavor.info "
186 "to choose between them — point detect() at a flavor directory "
187 "(_retail_, _classic_era_, ...)", infoPath->string(), products, clientPath.string()));
190 const BuildInfoRow& row = rows.front();
191 auto version = parseVersion(row.version);
194 std::format(
"'{}' records an unparseable version '{}' for " "product '{}'", infoPath->string(),
195 row.version, row.product));
196 version->flavor = flavorOf(row.product);
198 return ClientInstall{.path = std::move(clientPath), .version = *
version, .cascProduct = row.product};
Reading a client installation's own identity off disk: which product it is, which build,...
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
@ Retail
The progression client ('wow').
@ Anniversary
The Anniversary realms ('wow_anniversary').
@ ClassicEra
Classic Era ('wow_classic_era'): the 1.13-1.15 vanilla realms.
@ Classic
The Classic progression line ('wow_classic'): BCC 2.5, WotLK 3.4, Cata 4.4, MoP 5....
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ NotSupported
Operation not supported by this backend/provider.
@ IoError
Generic filesystem I/O failure (project directory).
static Result< ClientInstall > detect(std::filesystem::path clientPath)
ClientVersion version
The exact installed version, flavor included.