13 namespace fsys = std::filesystem;
24 constexpr std::array WotlkBase{
54 constexpr std::array VanillaBase{
83 constexpr std::array TbcBase{
104 constexpr std::array CataBase{
131 constexpr std::array MopBase{
159 constexpr std::array ChainSpecs{
167 std::string expandLocale(std::string_view pattern, std::string_view code) {
169 out.reserve(pattern.size() + code.size());
170 for (std::size_t i = 0; i < pattern.size();) {
171 if (pattern.compare(i, 8,
"{locale}") == 0) {
175 else out += pattern[i++];
180 constexpr char asciiLower(
char c) {
181 return (c >=
'A' && c <=
'Z') ?
static_cast<char>(c -
'A' +
'a') : c;
184 bool ciEqual(std::string_view a, std::string_view b) {
185 return a.size() == b.size() && std::ranges::equal(a, b, [](
char x,
char y) {
186 return asciiLower(x) == asciiLower(y);
194 bool ciLess(std::string_view a, std::string_view b) {
195 return std::ranges::lexicographical_compare(a, b, [](
char x,
char y) {
return asciiLower(x) < asciiLower(y); });
200 std::optional<std::string_view> mpqCore(std::string_view name) {
201 if (name.size() < 4 || !ciEqual(name.substr(name.size() - 4),
".MPQ"))
return std::nullopt;
202 return name.substr(0, name.size() - 4);
208 bool isPatchCore(std::string_view core, std::string_view stem) {
209 if (ciEqual(core, stem))
return true;
210 return core.size() == stem.size() + 2 && core[stem.size()] ==
'-' && ciEqual(core.substr(0, stem.size()), stem);
217 void pushBase(std::vector<ChainMember>& out, fsys::path candidate) {
219 if (fsys::is_regular_file(candidate, ec)) out.push_back({std::move(candidate),
false});
220 else if (fsys::is_directory(candidate, ec)) out.push_back({std::move(candidate),
true});
227 std::optional<std::uint32_t> updateBuild(std::string_view core) {
228 constexpr std::string_view stem =
"wow-update-";
229 if (core.size() <= stem.size() || !ciEqual(core.substr(0, stem.size()), stem))
return std::nullopt;
230 const auto dash = core.rfind(
'-');
231 const std::string_view tail = core.substr(dash + 1);
232 if (tail.empty())
return std::nullopt;
233 std::uint32_t build = 0;
234 for (
const char c : tail) {
235 if (c <
'0' || c >
'9')
return std::nullopt;
236 build = build * 10 +
static_cast<std::uint32_t
>(c -
'0');
245 void collectUpdates(std::vector<std::pair<std::uint32_t, ChainMember>>& found,
246 const fsys::path& dir,
247 std::string_view prefix) {
249 for (
const auto& entry : fsys::directory_iterator{dir, ec}) {
250 const std::string name = entry.path().filename().string();
251 const auto core = mpqCore(name);
253 const auto build = updateBuild(*core);
254 if (!build || !entry.is_regular_file(ec))
continue;
256 *build,
ChainMember{.path = entry.path(), .incremental =
true, .prefix = std::string{prefix}});
267 void collectPatches(std::vector<std::pair<std::string, ChainMember>>& found,
268 const fsys::path& dir,
269 std::string_view stem) {
271 for (
const auto& entry : fsys::directory_iterator{dir, ec}) {
272 const std::string name = entry.path().filename().string();
273 const auto core = mpqCore(name);
274 if (!core || !isPatchCore(*core, stem))
continue;
275 const bool isDir = entry.is_directory(ec);
276 if (!isDir && !entry.is_regular_file(ec))
continue;
277 found.emplace_back(std::string{*core},
ChainMember{entry.path(), isDir});
283 for (
const auto& spec : ChainSpecs)
284 if (spec.version.build == version.
build)
return &spec;
285 for (
const auto& spec : ChainSpecs)
286 if (spec.version.major == version.
major && spec.version.minor == version.
minor && spec.version.patch == version.
294 const fsys::path localeDir = dataDir / code;
296 std::vector<ChainMember> out;
300 switch (entry.
kind) {
302 pushBase(out, dataDir / entry.
pattern);
305 pushBase(out, localeDir / expandLocale(entry.
pattern, code));
316 std::vector<std::pair<std::string, ChainMember>> patches;
317 collectPatches(patches, dataDir,
"patch");
318 collectPatches(patches, localeDir, std::format(
"patch-{}", code));
319 std::ranges::stable_sort(patches, ciLess, &std::pair<std::string, ChainMember>::first);
320 for (
auto& [key, member] : patches) out.push_back(std::move(member));
329 std::vector<std::pair<std::uint32_t, ChainMember>> updates;
330 collectUpdates(updates, dataDir,
"base");
331 collectUpdates(updates, localeDir, code);
332 std::ranges::stable_sort(updates, {}, &std::pair<std::uint32_t, ChainMember>::first);
333 for (
auto& [build, member] : updates) out.push_back(std::move(member));
Per-version MPQ archive chain tables and their expansion against a real or fake Data/ directory.
@ Fixed
One archive in Data/, e.g.
@ LocaleFixed
One archive in Data/{locale}/, "{locale}" expanded.
const MpqChainSpec * findChainSpec(const ClientVersion &version)
The chain spec for version: exact build match first, then major.minor.patch.
@ UpdateChain
cata/MoP incremental updates: wow-update-{build}.MPQ / wow-update-base-{build}.MPQ in Data/ and wow-u...
@ ClassicWildcard
Pre-Cata wildcard patch tier.
Result< std::vector< ChainMember > > expandChain(const MpqChainSpec &spec, const std::filesystem::path &dataDir, Locale locale)
Expand a chain spec against a Data directory into concrete members in load order (lowest -> highest p...
constexpr ClientVersion Mop
constexpr ClientVersion Tbc
constexpr ClientVersion Vanilla
constexpr ClientVersion Cata
constexpr ClientVersion Wotlk
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::string_view localeCode(Locale locale)
The four-letter code of locale ("enUS", ...) as used in MPQ locale directory and archive names.
std::uint32_t build
Exact client build number, e.g.
std::uint16_t major
Expansion number, e.g.
std::uint16_t minor
Minor version within the expansion.
One resolved member of a chain, in load order.
A client version's archive chain: the fixed base tier plus how its patch tier is found.
std::span< const ChainEntry > baseEntries