entity_position_profile_build.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #include "entity_position_profile_build.h"
  2. #include <Windows.h>
  3. #include <algorithm>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <cstring>
  9. #include <span>
  10. #include <string_view>
  11. #include <vector>
  12. #include "../../../core/filesystem/path.h"
  13. #include "../../../core/logging/log.h"
  14. #include "../../../middleware/content/packages/named_tags.h"
  15. #include "../../../middleware/content/packages/tables/entity_position_profile_extractor.h"
  16. #include "../../../state/build_data/runtime.h"
  17. #include "../../../state/content_manifest/content_manifest_state_runtime.h"
  18. #include "../../../state/gameplay/external/entity_position_profiles.h"
  19. #include "entity_object_type_build.h"
  20. namespace sunrise::client::content::activity::entity_position_profiles {
  21. namespace {
  22. namespace profiles = state::gameplay::entity_position_profiles;
  23. namespace extractor = middleware::content::packages::position_profiles;
  24. namespace named = middleware::content::packages::named_tags;
  25. namespace reader = middleware::content::packages::reader;
  26. using Blob = std::vector<std::byte>;
  27. /** One installed package directory never holds more `.pkg` files than this. */
  28. constexpr std::size_t kMaximumPackageFileCount = 65536;
  29. /** Package files carry the header prefix this pass reads before any metadata. */
  30. constexpr std::size_t kHeaderBytes = 0x180;
  31. /** The hash64 reference directory is refused above this size. */
  32. constexpr std::uint64_t kMaximumMetadataBytes = 64ULL * 1024ULL * 1024ULL;
  33. /** Package file names carry this extension and nothing else is read. */
  34. constexpr std::wstring_view kPackageExtension = L".pkg";
  35. struct Name final {
  36. extractor::NamedTag value;
  37. std::uint32_t patch{};
  38. bool conflict{};
  39. };
  40. struct Names final {
  41. /** Sorted by name so the merge order matches the emitted row order. */
  42. std::vector<Name> rows;
  43. bool base{};
  44. std::uint32_t patch{};
  45. };
  46. struct Context final {
  47. const reader::Source& source;
  48. reader::Scratch& scratch;
  49. };
  50. /** One installed package file. The family is the leading part of the name before its patch. */
  51. struct File final {
  52. std::array<wchar_t, MAX_PATH> name{};
  53. std::size_t familyLength{};
  54. std::uint32_t patch{};
  55. };
  56. /** One hash64 metadata reference and the package row it names. */
  57. struct Reference final {
  58. std::uint64_t key{};
  59. std::uint32_t tag{};
  60. std::uint32_t classId{};
  61. };
  62. /** One merged reference. A key that two families disagree on is not emitted. */
  63. struct MergedReference final {
  64. Reference row{};
  65. bool unique{true};
  66. };
  67. /** Closes one package handle on every exit path. */
  68. class FileHandle final {
  69. public:
  70. explicit FileHandle(const wchar_t* path) noexcept
  71. : handle_(CreateFileW(path,
  72. GENERIC_READ,
  73. FILE_SHARE_READ,
  74. nullptr,
  75. OPEN_EXISTING,
  76. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
  77. nullptr)) {}
  78. ~FileHandle() noexcept {
  79. if (handle_ != INVALID_HANDLE_VALUE) {
  80. (void)CloseHandle(handle_);
  81. }
  82. }
  83. FileHandle(const FileHandle&) = delete;
  84. FileHandle& operator=(const FileHandle&) = delete;
  85. [[nodiscard]] bool valid() const noexcept {
  86. return handle_ != INVALID_HANDLE_VALUE;
  87. }
  88. [[nodiscard]] HANDLE get() const noexcept {
  89. return handle_;
  90. }
  91. private:
  92. HANDLE handle_{INVALID_HANDLE_VALUE};
  93. };
  94. /** @return The family part of one package file name. */
  95. [[nodiscard]] std::wstring_view family_of(const File& file) noexcept {
  96. return {file.name.data(), file.familyLength};
  97. }
  98. /** Joins one directory and child without doubling a separator an extended path would keep. */
  99. [[nodiscard]] bool join_path(std::wstring_view directory,
  100. std::wstring_view child,
  101. core::path::Buffer& output) noexcept {
  102. if (!core::path::assign(output, directory)) {
  103. return false;
  104. }
  105. if (!directory.empty() && directory.back() != L'\\' && directory.back() != L'/'
  106. && !core::path::append(output, L"\\")) {
  107. return false;
  108. }
  109. return core::path::append(output, child);
  110. }
  111. /**
  112. * Reads one exact byte range. A short read is a failure.
  113. * @param file Open package handle.
  114. * @param offset Absolute byte offset.
  115. * @param output Receives the range; its size is the amount required.
  116. * @return True when the whole range was read.
  117. */
  118. [[nodiscard]] bool
  119. read_at(HANDLE file, std::uint64_t offset, std::span<std::byte> output) noexcept {
  120. LARGE_INTEGER position{};
  121. position.QuadPart = static_cast<LONGLONG>(offset);
  122. if (SetFilePointerEx(file, position, nullptr, FILE_BEGIN) == FALSE) {
  123. return false;
  124. }
  125. std::size_t done = 0;
  126. while (done < output.size()) {
  127. // One ReadFile call is bounded by its DWORD length.
  128. constexpr std::size_t kMaximumChunk = 0x10000000;
  129. const auto chunk = static_cast<DWORD>((std::min)(output.size() - done, kMaximumChunk));
  130. DWORD read = 0;
  131. if (ReadFile(file, output.data() + done, chunk, &read, nullptr) == FALSE || read == 0) {
  132. return false;
  133. }
  134. done += read;
  135. }
  136. return true;
  137. }
  138. /** The manifest identity includes the installed package builds. */
  139. bool fingerprint(void* opaque, const state::content_manifest::View& view) noexcept {
  140. std::copy(view.buildFingerprint.begin(),
  141. view.buildFingerprint.end(),
  142. static_cast<profiles::Fingerprint*>(opaque)->begin());
  143. return true;
  144. }
  145. /** Same-patch name conflicts cannot select an arbitrary package. */
  146. bool collect_name(void* opaque, const named::Entry& entry) noexcept {
  147. try {
  148. auto& names = *static_cast<Names*>(opaque);
  149. if (entry.classId != 0x808091DE && entry.classId != 0x80809994) {
  150. return true;
  151. }
  152. const std::string_view name(entry.name.data(), entry.nameLength);
  153. const auto found = std::lower_bound(
  154. names.rows.begin(), names.rows.end(), name, [](const Name& row, std::string_view key) {
  155. return row.value.text() < key;
  156. });
  157. const Name replacement{{name, entry.tag, entry.classId, names.base}, names.patch, false};
  158. if (found == names.rows.end() || found->value.text() != name) {
  159. names.rows.insert(found, replacement);
  160. } else if (found->patch < names.patch) {
  161. *found = replacement;
  162. } else if (found->patch == names.patch
  163. && (found->value.tag != entry.tag || found->value.classId != entry.classId)) {
  164. found->conflict = true;
  165. }
  166. return true;
  167. } catch (...) {
  168. return false;
  169. }
  170. }
  171. template <class T> T value(const Blob& bytes, std::size_t offset) {
  172. if (offset > bytes.size() || sizeof(T) > bytes.size() - offset) {
  173. throw 0;
  174. }
  175. T result{};
  176. std::memcpy(&result, bytes.data() + offset, sizeof result);
  177. return result;
  178. }
  179. /** Replaces one family reference, keeping the bank sorted by key. */
  180. void set_reference(std::vector<Reference>& rows, const Reference& row) {
  181. const auto found = std::lower_bound(
  182. rows.begin(), rows.end(), row.key, [](const Reference& left, std::uint64_t key) {
  183. return left.key < key;
  184. });
  185. if (found != rows.end() && found->key == row.key) {
  186. *found = row;
  187. return;
  188. }
  189. rows.insert(found, row);
  190. }
  191. /** Folds one family's references in, marking any key two families disagree on. */
  192. void merge_references(const std::vector<Reference>& family, std::vector<MergedReference>& merged) {
  193. for (const Reference& row : family) {
  194. const auto found = std::lower_bound(
  195. merged.begin(),
  196. merged.end(),
  197. row.key,
  198. [](const MergedReference& left, std::uint64_t key) { return left.row.key < key; });
  199. if (found == merged.end() || found->row.key != row.key) {
  200. merged.insert(found, MergedReference{row, true});
  201. } else if (found->row.tag != row.tag || found->row.classId != row.classId) {
  202. found->unique = false;
  203. }
  204. }
  205. }
  206. /** Parses one `<family>_<patch>.pkg` file name. @return False when the name has no patch suffix. */
  207. [[nodiscard]] bool parse_file_name(std::wstring_view name, File& output) noexcept {
  208. output = {};
  209. if (name.size() <= kPackageExtension.size() || name.size() >= output.name.size()
  210. || name.substr(name.size() - kPackageExtension.size()) != kPackageExtension) {
  211. return false;
  212. }
  213. const std::wstring_view stem = name.substr(0, name.size() - kPackageExtension.size());
  214. const std::size_t separator = stem.rfind(L'_');
  215. if (separator == std::wstring_view::npos || separator + 1 == stem.size()) {
  216. return false;
  217. }
  218. std::uint64_t patch = 0;
  219. for (const wchar_t character : stem.substr(separator + 1)) {
  220. if (character < L'0' || character > L'9') {
  221. return false;
  222. }
  223. patch = patch * 10U + static_cast<std::uint64_t>(character - L'0');
  224. if (patch > UINT32_MAX) {
  225. return false;
  226. }
  227. }
  228. std::copy(name.begin(), name.end(), output.name.begin());
  229. output.familyLength = separator;
  230. output.patch = static_cast<std::uint32_t>(patch);
  231. return true;
  232. }
  233. /** Collects every installed package file in family and patch order. */
  234. [[nodiscard]] bool collect_files(std::wstring_view directory, std::vector<File>& files) {
  235. core::path::Buffer search{};
  236. if (!join_path(directory, L"*", search)) {
  237. return false;
  238. }
  239. WIN32_FIND_DATAW entry{};
  240. const HANDLE find = FindFirstFileW(search.chars.data(), &entry);
  241. if (find == INVALID_HANDLE_VALUE) {
  242. return false;
  243. }
  244. bool complete = true;
  245. do {
  246. if ((entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
  247. continue;
  248. }
  249. File file{};
  250. const std::wstring_view name(entry.cFileName);
  251. if (name.size() <= kPackageExtension.size()
  252. || name.substr(name.size() - kPackageExtension.size()) != kPackageExtension) {
  253. continue;
  254. }
  255. if (files.size() >= kMaximumPackageFileCount || !parse_file_name(name, file)) {
  256. complete = false;
  257. break;
  258. }
  259. files.push_back(file);
  260. } while (FindNextFileW(find, &entry) != FALSE);
  261. (void)FindClose(find);
  262. if (!complete || files.empty()) {
  263. return false;
  264. }
  265. std::sort(files.begin(), files.end(), [](const File& a, const File& b) {
  266. return family_of(a) < family_of(b) || (family_of(a) == family_of(b) && a.patch < b.patch);
  267. });
  268. return true;
  269. }
  270. /** Metadata references merge patches within a family before cross-family conflict checks. */
  271. bool inventory(std::wstring_view directory,
  272. std::vector<extractor::NamedTag>& names,
  273. std::vector<extractor::KeyTag>& keys) {
  274. std::vector<File> files;
  275. if (!collect_files(directory, files)) {
  276. return false;
  277. }
  278. Names collected;
  279. std::vector<Reference> family;
  280. std::vector<MergedReference> merged;
  281. std::wstring_view currentFamily{};
  282. core::path::Buffer path{};
  283. for (const auto& file : files) {
  284. if (family_of(file) != currentFamily) {
  285. merge_references(family, merged);
  286. family.clear();
  287. currentFamily = family_of(file);
  288. }
  289. collected.base = currentFamily.find(L"_activities_") == std::wstring_view::npos;
  290. collected.patch = file.patch;
  291. named::Result result{};
  292. if (!join_path(directory, std::wstring_view(file.name.data()), path)
  293. || !named::extract_file(path.chars.data(), &collect_name, &collected, result)) {
  294. return false;
  295. }
  296. const FileHandle handle(path.chars.data());
  297. LARGE_INTEGER length{};
  298. if (!handle.valid() || GetFileSizeEx(handle.get(), &length) == FALSE
  299. || length.QuadPart <= 0) {
  300. return false;
  301. }
  302. Blob header(kHeaderBytes);
  303. if (!read_at(handle.get(), 0, header) || value<std::uint16_t>(header, 0) != 38) {
  304. return false;
  305. }
  306. /** Beta metadata has no hash64 reference directory at offset 48. */
  307. if (value<std::uint8_t>(header, 0x1A) == 0) {
  308. continue;
  309. }
  310. if (value<std::uint8_t>(header, 0x1A) != 1) {
  311. return false;
  312. }
  313. const auto offset = value<std::uint32_t>(header, 0xF0),
  314. size = value<std::uint32_t>(header, 0xF4);
  315. if (size == 0) {
  316. continue;
  317. }
  318. if (size > kMaximumMetadataBytes
  319. || static_cast<std::uint64_t>(offset) + size
  320. > static_cast<std::uint64_t>(length.QuadPart)) {
  321. return false;
  322. }
  323. Blob metadata(size);
  324. if (!read_at(handle.get(), offset, metadata)) {
  325. return false;
  326. }
  327. if (metadata.size() < 64) {
  328. continue;
  329. }
  330. std::vector<std::size_t> offsets;
  331. if (!extractor::array(metadata, 48, 16, 0x80809D02, offsets)) {
  332. return false;
  333. }
  334. for (auto member : offsets) {
  335. set_reference(family,
  336. {value<std::uint64_t>(metadata, member),
  337. value<std::uint32_t>(metadata, member + 8),
  338. value<std::uint32_t>(metadata, member + 12)});
  339. }
  340. }
  341. merge_references(family, merged);
  342. for (const MergedReference& row : merged) {
  343. if (row.unique) {
  344. keys.push_back({row.row.key, row.row.tag, row.row.classId});
  345. }
  346. }
  347. for (const Name& row : collected.rows) {
  348. if (!row.conflict) {
  349. names.push_back(row.value);
  350. }
  351. }
  352. return true;
  353. }
  354. /** Class checks apply to every live tag reached by the extraction. */
  355. bool read(void* opaque, std::uint32_t tag, std::uint32_t expected, Blob& bytes) noexcept {
  356. auto& context = *static_cast<Context*>(opaque);
  357. std::uint32_t actual{};
  358. return reader::read_tag(context.source, context.scratch, tag, bytes, actual)
  359. && actual == expected;
  360. }
  361. } // namespace
  362. bool ready() noexcept {
  363. profiles::Fingerprint identity{};
  364. if (!state::content_manifest::visit_snapshot(&fingerprint, &identity)) {
  365. return false;
  366. }
  367. const bool positions = profiles::confirm(identity);
  368. const bool objects = state::gameplay::entity_object_types::confirm(identity);
  369. return positions && objects;
  370. }
  371. /** The package pass confirms shared-cache rows or publishes a complete extraction. */
  372. bool build(const reader::Source& source, reader::Scratch& scratch) noexcept {
  373. try {
  374. profiles::Fingerprint identity{};
  375. if (!state::content_manifest::visit_snapshot(&fingerprint, &identity)) {
  376. return false;
  377. }
  378. const bool positions = profiles::confirm(identity);
  379. const bool objects = state::gameplay::entity_object_types::confirm(identity);
  380. if (positions && objects) {
  381. return true;
  382. }
  383. if (!objects && !entity_object_types::build(source, scratch, identity)) {
  384. return false;
  385. }
  386. if (positions) {
  387. state::build_data::invalidate_cache();
  388. return true;
  389. }
  390. profiles::reset();
  391. std::vector<extractor::NamedTag> names;
  392. std::vector<extractor::KeyTag> keys;
  393. profiles::Rows rows;
  394. Context context{source, scratch};
  395. if (!inventory(source.directory, names, keys)
  396. || !extractor::extract(names, keys, &read, &context, rows)) {
  397. return false;
  398. }
  399. const auto count = rows.size();
  400. const bool published = profiles::publish(std::move(rows), identity);
  401. if (published) {
  402. state::build_data::invalidate_cache();
  403. }
  404. char line[160]{};
  405. (void)std::snprintf(
  406. line, sizeof line, "entity_position_profiles source=packages rows=%zu", count);
  407. core::log::write(core::log::Channel::client, core::log::Level::info, line);
  408. return published;
  409. } catch (...) {
  410. return false;
  411. }
  412. }
  413. } // namespace sunrise::client::content::activity::entity_position_profiles