wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
client_install.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <charconv>
6#include <format>
7#include <fstream>
8#include <optional>
9#include <ranges>
10#include <string_view>
11#include <vector>
12
13namespace wowlib::fs {
14 namespace {
15 namespace fsys = std::filesystem;
16
21 std::vector<std::string_view> splitFields(std::string_view line) {
22 std::vector<std::string_view> fields;
23 while (true) {
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);
28 }
29 fields.push_back(line);
30 return fields;
31 }
32
37 std::string_view columnName(std::string_view headerField) {
38 return headerField.substr(0, headerField.find('!'));
39 }
40
45 std::optional<std::vector<std::string>> readLines(const fsys::path& path) {
46 std::ifstream file{path};
47 if (!file) return std::nullopt;
48
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));
53 }
54 return lines;
55 }
56
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;
66 return std::nullopt;
67 }
68
74 std::optional<fsys::path> findBuildInfo(const fsys::path& clientPath) {
75 std::error_code ec;
76 for (const fsys::path& candidate : {clientPath / ".build.info", clientPath.parent_path() / ".build.info"})
77 if (fsys::is_regular_file(candidate, ec)) return candidate;
78 return std::nullopt;
79 }
80
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);
93 }
94 return ClientVersion{
95 static_cast<std::uint16_t>(parts[0]),
96 static_cast<std::uint16_t>(parts[1]),
97 static_cast<std::uint16_t>(parts[2]),
98 parts[3]
99 };
100 }
101
107 ClientFlavor flavorOf(std::string_view product) {
108 if (product.starts_with("wow_classic_era")) return ClientFlavor::ClassicEra;
109 if (product.starts_with("wow_anniversary")) return ClientFlavor::Anniversary;
110 if (product.starts_with("wow_classic")) return ClientFlavor::Classic;
112 }
113
115 struct BuildInfoRow {
116 std::string product;
117 std::string version;
118 bool active = false;
119 };
120
124 std::vector<BuildInfoRow> parseBuildInfo(const std::vector<std::string>& lines) {
125 if (lines.empty()) return {};
126
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;
131 });
132 return static_cast<std::size_t>(found - header.begin());
133 };
134 const std::size_t productAt = column("Product");
135 const std::size_t versionAt = column("Version");
136 const std::size_t activeAt = column("Active");
137
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;
142 rows.push_back({
143 .product = std::string{fields[productAt]},
144 .version = std::string{fields[versionAt]},
145 .active = activeAt < fields.size() && fields[activeAt] != "0"
146 });
147 }
148 return rows;
149 }
150 }
151
153 const auto infoPath = findBuildInfo(clientPath);
154 if (!infoPath)
156 std::format(
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()));
160
161 const auto lines = readLines(*infoPath);
162 if (!lines)
163 return makeError(ErrorCode::IoError, std::format("cannot read '{}'", infoPath->string()));
164
165 auto rows = parseBuildInfo(*lines);
166 std::erase_if(rows, [](const BuildInfoRow& row) { return !row.active; });
167 if (rows.empty())
168 return makeError(ErrorCode::NotSupported, std::format("'{}' lists no active installation", infoPath->string()));
169
170 // A multi-flavor install shares one table across its flavor directories;
171 // .flavor.info beside Data/ says which row is THIS one.
172 const auto flavor = readFlavor(clientPath);
173 if (flavor) {
174 const auto match = std::ranges::find(rows, *flavor, &BuildInfoRow::product);
175 if (match == rows.end())
176 return makeError(ErrorCode::NotSupported, std::format("'{}' declares product '{}', which '{}' does not list",
177 (clientPath / ".flavor.info").string(), *flavor,
178 infoPath->string()));
179 rows = {*match};
180 }
181 else if (rows.size() > 1) {
182 std::string products;
183 for (const BuildInfoRow& row : rows) products += (products.empty() ? "" : ", ") + row.product;
184 return makeError(ErrorCode::NotSupported, std::format(
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()));
188 }
189
190 const BuildInfoRow& row = rows.front();
191 auto version = parseVersion(row.version);
192 if (!version)
194 std::format("'{}' records an unparseable version '{}' for " "product '{}'", infoPath->string(),
195 row.version, row.product));
196 version->flavor = flavorOf(row.product);
197
198 return ClientInstall{.path = std::move(clientPath), .version = *version, .cascProduct = row.product};
199 }
200}
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-...
Definition error.hpp:100
@ 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.
Definition error.hpp:107
@ NotSupported
Operation not supported by this backend/provider.
Definition error.hpp:32
@ IoError
Generic filesystem I/O failure (project directory).
Definition error.hpp:30
static Result< ClientInstall > detect(std::filesystem::path clientPath)
ClientVersion version
The exact installed version, flavor included.