activity_sdk_lua_publish.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <atomic>
  5. #include <cctype>
  6. #include <cstdio>
  7. #include <limits>
  8. #include <span>
  9. #include <string>
  10. #include <string_view>
  11. #include <vector>
  12. #include "../../../core/filesystem/path.h"
  13. #include "../../../core/filesystem/temporary_sibling.h"
  14. #include "../../../state/activity_sdk/runtime.h"
  15. #include "activity_sdk_lua_artifacts.h"
  16. #include "activity_sdk_lua_artifacts_internal.h"
  17. namespace sunrise::client::content::activity::sdk_generation::lua_artifacts {
  18. namespace {
  19. // Published Lua tree layout below the artifact root.
  20. constexpr std::wstring_view kLuaSuffix = L"\\lua";
  21. constexpr std::wstring_view kActivitiesSuffix = L"\\activities";
  22. constexpr std::wstring_view kMissionsSuffix = L"\\missions";
  23. constexpr std::wstring_view kSunriseSuffix = L"\\sunrise";
  24. constexpr std::wstring_view kTemporarySuffix = L".%08lX.%08lX.%08lX.tmp";
  25. // Distinct temporary names tried before publication fails.
  26. constexpr std::size_t kTemporaryAttempts = 16;
  27. volatile LONG g_temporarySequence{};
  28. /** One small handle owner keeps failed publication paths closed. */
  29. class FileHandle final {
  30. public:
  31. explicit FileHandle(HANDLE value) noexcept : value_(value) {}
  32. FileHandle(const FileHandle&) = delete;
  33. FileHandle& operator=(const FileHandle&) = delete;
  34. ~FileHandle() noexcept {
  35. if (value_ != INVALID_HANDLE_VALUE) {
  36. (void)CloseHandle(value_);
  37. }
  38. }
  39. [[nodiscard]] HANDLE get() const noexcept {
  40. return value_;
  41. }
  42. [[nodiscard]] bool close() noexcept {
  43. if (value_ == INVALID_HANDLE_VALUE) {
  44. return true;
  45. }
  46. const HANDLE value = value_;
  47. value_ = INVALID_HANDLE_VALUE;
  48. return CloseHandle(value) != FALSE;
  49. }
  50. private:
  51. HANDLE value_{INVALID_HANDLE_VALUE};
  52. };
  53. [[nodiscard]] bool ensure_directory(const wchar_t* path) noexcept {
  54. if (CreateDirectoryW(path, nullptr) != FALSE) {
  55. return true;
  56. }
  57. if (GetLastError() != ERROR_ALREADY_EXISTS) {
  58. return false;
  59. }
  60. const DWORD attributes = GetFileAttributesW(path);
  61. return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
  62. }
  63. [[nodiscard]] bool child_path(std::wstring_view parent,
  64. std::wstring_view suffix,
  65. core::path::Buffer& output) noexcept {
  66. return core::path::assign(output, parent) && core::path::append(output, suffix);
  67. }
  68. /** Forms a process-, thread-, and sequence-scoped sibling name for publication. */
  69. [[nodiscard]] bool temporary_path(const wchar_t* finalPath, core::path::Buffer& output) noexcept {
  70. std::array<wchar_t, 64> suffix{};
  71. const DWORD sequence = static_cast<DWORD>(InterlockedIncrement(&g_temporarySequence));
  72. const int length = std::swprintf(suffix.data(),
  73. suffix.size(),
  74. kTemporarySuffix.data(),
  75. GetCurrentProcessId(),
  76. GetCurrentThreadId(),
  77. sequence);
  78. return length > 0 && static_cast<std::size_t>(length) < suffix.size()
  79. && core::path::assign(output, finalPath)
  80. && core::path::append(output, std::wstring_view(suffix.data(), length));
  81. }
  82. /** Writes the complete source in DWORD-sized chunks and rejects short writes. */
  83. [[nodiscard]] bool write_all(HANDLE file, std::string_view source) noexcept {
  84. while (!source.empty()) {
  85. const std::size_t count =
  86. (std::min)(source.size(),
  87. static_cast<std::size_t>((std::numeric_limits<DWORD>::max)()));
  88. DWORD transferred = 0;
  89. if (WriteFile(file, source.data(), static_cast<DWORD>(count), &transferred, nullptr)
  90. == FALSE
  91. || transferred != count) {
  92. return false;
  93. }
  94. source.remove_prefix(count);
  95. }
  96. return true;
  97. }
  98. /** Publishes only a completely written, closed sibling file and removes it after failure. */
  99. [[nodiscard]] bool publish_file(const wchar_t* finalPath, std::string_view source) noexcept {
  100. core::path::remove_stale_siblings(finalPath);
  101. for (std::size_t attempt = 0; attempt < kTemporaryAttempts; ++attempt) {
  102. core::path::Buffer temporary{};
  103. if (!temporary_path(finalPath, temporary)) {
  104. return false;
  105. }
  106. const HANDLE raw = CreateFileW(temporary.chars.data(),
  107. GENERIC_WRITE,
  108. 0,
  109. nullptr,
  110. CREATE_NEW,
  111. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
  112. nullptr);
  113. if (raw == INVALID_HANDLE_VALUE) {
  114. const DWORD error = GetLastError();
  115. if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
  116. continue;
  117. }
  118. return false;
  119. }
  120. FileHandle file(raw);
  121. bool complete = write_all(file.get(), source);
  122. complete = file.close() && complete;
  123. if (!complete || !core::path::publish_sibling(temporary.chars.data(), finalPath)) {
  124. (void)DeleteFileW(temporary.chars.data());
  125. return false;
  126. }
  127. return true;
  128. }
  129. return false;
  130. }
  131. [[nodiscard]] bool add_bytes(std::uint64_t& total, std::string_view source) noexcept {
  132. if (source.size() > (std::numeric_limits<std::uint64_t>::max)() - total) {
  133. return false;
  134. }
  135. total += source.size();
  136. return true;
  137. }
  138. /** Reads one ordinary file only when its byte count matches the expected commit record. */
  139. [[nodiscard]] bool file_equals(const wchar_t* path, std::string_view expected) noexcept {
  140. if (path == nullptr || expected.size() > (std::numeric_limits<DWORD>::max)()) {
  141. return false;
  142. }
  143. const DWORD attributes = GetFileAttributesW(path);
  144. if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
  145. return false;
  146. }
  147. const HANDLE file = CreateFileW(path,
  148. GENERIC_READ,
  149. FILE_SHARE_READ | FILE_SHARE_DELETE,
  150. nullptr,
  151. OPEN_EXISTING,
  152. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
  153. nullptr);
  154. if (file == INVALID_HANDLE_VALUE) {
  155. return false;
  156. }
  157. LARGE_INTEGER size{};
  158. std::string actual;
  159. bool complete = GetFileSizeEx(file, &size) != FALSE && size.QuadPart >= 0
  160. && static_cast<std::uint64_t>(size.QuadPart) == expected.size();
  161. if (complete) {
  162. try {
  163. actual.resize(expected.size());
  164. } catch (...) {
  165. complete = false;
  166. }
  167. }
  168. DWORD read = 0;
  169. if (complete) {
  170. complete = ReadFile(file, actual.data(), static_cast<DWORD>(actual.size()), &read, nullptr)
  171. != FALSE
  172. && read == actual.size();
  173. }
  174. complete = CloseHandle(file) != FALSE && complete;
  175. return complete && actual == expected;
  176. }
  177. /** Copies one catalog digest into the fixed generator source field. */
  178. [[nodiscard]] bool copy_digest(std::span<const std::byte> input,
  179. std::array<std::byte, 32>& output) noexcept {
  180. if (input.size() != output.size()) {
  181. return false;
  182. }
  183. std::copy(input.begin(), input.end(), output.begin());
  184. return true;
  185. }
  186. [[nodiscard]] bool publish_named(std::wstring_view directory,
  187. std::wstring_view name,
  188. std::string_view source,
  189. std::uint64_t& bytes) noexcept {
  190. core::path::Buffer path{};
  191. return child_path(directory, name, path) && publish_file(path.chars.data(), source)
  192. && add_bytes(bytes, source);
  193. }
  194. /** Converts one generator-owned ASCII stem to a child Lua filename. */
  195. [[nodiscard]] bool module_name(const SourceModule& module, std::wstring& output) {
  196. if (module.stem.empty()) {
  197. return false;
  198. }
  199. output.clear();
  200. output.reserve(module.stem.size() + 5U);
  201. output.push_back(L'\\');
  202. for (const unsigned char byte : module.stem) {
  203. if (byte >= 0x80U || (std::isalnum(byte) == 0 && byte != '_')) {
  204. return false;
  205. }
  206. output.push_back(static_cast<wchar_t>(byte));
  207. }
  208. output.append(L".lua");
  209. return true;
  210. }
  211. /** Shared immutable module spans and atomic scheduling state for one file batch. */
  212. struct ModulePublishBatch final {
  213. std::wstring_view activityDirectory{};
  214. std::wstring_view missionDirectory{};
  215. std::span<const SourceModule> activities{};
  216. std::span<const SourceModule> missions{};
  217. std::atomic_size_t next{};
  218. std::atomic_uint64_t bytes{};
  219. std::atomic_bool failed{};
  220. };
  221. /** Uses most cores while leaving two for the running game and server. */
  222. [[nodiscard]] std::size_t publish_worker_count(std::size_t files) noexcept {
  223. SYSTEM_INFO info{};
  224. GetSystemInfo(&info);
  225. const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
  226. const std::size_t available = processors > 2U ? processors - 2U : 1U;
  227. return (std::min)(files, (std::min)(available, static_cast<std::size_t>(12)));
  228. }
  229. /** Publishes distinct module paths without serializing independent file writes. */
  230. void run_module_publish_worker(ModulePublishBatch& batch) noexcept {
  231. std::wstring fileName;
  232. std::uint64_t bytes = 0;
  233. const std::size_t total = batch.activities.size() + batch.missions.size();
  234. while (!batch.failed.load(std::memory_order_relaxed)) {
  235. const std::size_t index = batch.next.fetch_add(1U);
  236. if (index >= total) {
  237. break;
  238. }
  239. const bool activity = index < batch.activities.size();
  240. const SourceModule& module =
  241. activity ? batch.activities[index] : batch.missions[index - batch.activities.size()];
  242. const std::wstring_view directory =
  243. activity ? batch.activityDirectory : batch.missionDirectory;
  244. if (!module_name(module, fileName)
  245. || !publish_named(directory, fileName, module.source, bytes)) {
  246. batch.failed.store(true, std::memory_order_relaxed);
  247. break;
  248. }
  249. }
  250. batch.bytes.fetch_add(bytes, std::memory_order_relaxed);
  251. }
  252. /** Adapts one file publisher to the Windows thread ABI. */
  253. DWORD WINAPI module_publish_thread_main(void* opaque) noexcept {
  254. run_module_publish_worker(*static_cast<ModulePublishBatch*>(opaque));
  255. return 0;
  256. }
  257. /** Publishes all generated modules in parallel and returns their byte count. */
  258. [[nodiscard]] bool publish_modules(std::wstring_view activityDirectory,
  259. std::wstring_view missionDirectory,
  260. const Bundle& bundle,
  261. std::uint64_t& bytes) {
  262. ModulePublishBatch batch{};
  263. batch.activityDirectory = activityDirectory;
  264. batch.missionDirectory = missionDirectory;
  265. batch.activities = bundle.activityModules;
  266. batch.missions = bundle.missionModules;
  267. const std::size_t total = batch.activities.size() + batch.missions.size();
  268. const std::size_t workers = publish_worker_count(total);
  269. if (workers == 0) {
  270. return false;
  271. }
  272. std::vector<HANDLE> threads;
  273. threads.reserve(workers - 1U);
  274. for (std::size_t worker = 1; worker < workers; ++worker) {
  275. const HANDLE thread =
  276. CreateThread(nullptr, 0, &module_publish_thread_main, &batch, 0, nullptr);
  277. if (thread != nullptr) {
  278. threads.push_back(thread);
  279. }
  280. }
  281. run_module_publish_worker(batch);
  282. for (const HANDLE thread : threads) {
  283. (void)WaitForSingleObject(thread, INFINITE);
  284. (void)CloseHandle(thread);
  285. }
  286. if (batch.failed.load(std::memory_order_relaxed)) {
  287. return false;
  288. }
  289. const std::uint64_t moduleBytes = batch.bytes.load(std::memory_order_relaxed);
  290. if (moduleBytes > (std::numeric_limits<std::uint64_t>::max)() - bytes) {
  291. return false;
  292. }
  293. bytes += moduleBytes;
  294. return true;
  295. }
  296. } // namespace
  297. /** Publishes each file atomically; a later failure can leave earlier files updated. */
  298. Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result& output) noexcept {
  299. output = {};
  300. if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0' || bundle.activityIndex.empty()
  301. || bundle.missionIndex.empty() || bundle.activitySdkModule.empty()
  302. || bundle.behaviorModule.empty() || bundle.manifestJson.empty()
  303. || bundle.activityModules.empty() || bundle.missionModules.empty()) {
  304. return Status::invalidInput;
  305. }
  306. try {
  307. core::path::Buffer lua{};
  308. core::path::Buffer activities{};
  309. core::path::Buffer missions{};
  310. core::path::Buffer sunrise{};
  311. if (!child_path(sdkDirectory, kLuaSuffix, lua)
  312. || !child_path(lua.chars.data(), kActivitiesSuffix, activities)
  313. || !child_path(lua.chars.data(), kMissionsSuffix, missions)
  314. || !child_path(lua.chars.data(), kSunriseSuffix, sunrise)
  315. || !ensure_directory(lua.chars.data()) || !ensure_directory(activities.chars.data())
  316. || !ensure_directory(missions.chars.data())
  317. || !ensure_directory(sunrise.chars.data())) {
  318. return Status::directoryFailure;
  319. }
  320. std::uint64_t bytes = 0;
  321. if (!publish_named(lua.chars.data(), L"\\manifest.json", bundle.manifestJson, bytes)
  322. || !publish_named(
  323. sunrise.chars.data(), L"\\activity_sdk.lua", bundle.activitySdkModule, bytes)
  324. || !publish_named(
  325. sunrise.chars.data(), L"\\behaviors.lua", bundle.behaviorModule, bytes)) {
  326. return Status::writeFailure;
  327. }
  328. if (!publish_modules(activities.chars.data(), missions.chars.data(), bundle, bytes)) {
  329. return Status::writeFailure;
  330. }
  331. if (!publish_named(lua.chars.data(), L"\\missions.lua", bundle.missionIndex, bytes)
  332. || !publish_named(lua.chars.data(), L"\\activities.lua", bundle.activityIndex, bytes)) {
  333. return Status::writeFailure;
  334. }
  335. const std::size_t sourceFiles =
  336. bundle.activityModules.size() + bundle.missionModules.size();
  337. if (sourceFiles > format::kAbsentIndex - 5U) {
  338. return Status::writeFailure;
  339. }
  340. output.activityCount = static_cast<std::uint32_t>(bundle.activityModules.size());
  341. output.fileCount = static_cast<std::uint32_t>(sourceFiles + 5U);
  342. output.byteCount = bytes;
  343. return Status::ready;
  344. } catch (...) {
  345. output = {};
  346. return Status::writeFailure;
  347. }
  348. }
  349. Status publish(const wchar_t* sdkDirectory, const Source& source, Result& output) noexcept {
  350. output = {};
  351. Bundle bundle{};
  352. const Status built = build(source, bundle);
  353. return built == Status::ready ? publish_bundle(sdkDirectory, bundle, output) : built;
  354. }
  355. /** @return True when the published Lua modules already match this catalog. */
  356. bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog& catalog) noexcept {
  357. if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0') {
  358. return false;
  359. }
  360. try {
  361. Source source{};
  362. if (!copy_digest(catalog.sdk_build_sha256(), source.sdkBuildSha256)
  363. || !copy_digest(catalog.payload_sha256(), source.sdkPayloadSha256)
  364. || !copy_digest(catalog.content_key_sha256(), source.contentKeySha256)
  365. || !copy_digest(catalog.logical_ir_sha256(), source.logicalIrSha256)) {
  366. return false;
  367. }
  368. source.activities = catalog.activities();
  369. source.states = catalog.states();
  370. source.slots = catalog.slots();
  371. source.squads = catalog.squads();
  372. source.authoredSceneResources = catalog.authored_scene_resources();
  373. source.authoredSceneSquadEdges = catalog.authored_scene_squad_edges();
  374. source.taskTargets = catalog.task_targets();
  375. source.dialogueCueTexts = catalog.dialogue_cue_texts();
  376. source.directiveElements = catalog.directive_elements();
  377. source.behaviorPrograms = catalog.behavior_programs();
  378. source.behaviorInputs = catalog.behavior_inputs();
  379. source.behaviorChannelWrites = catalog.behavior_channel_writes();
  380. source.behaviorOwners = catalog.behavior_owners();
  381. source.behaviorActivityBindings = catalog.behavior_activity_bindings();
  382. source.actorClasses = catalog.actor_classes();
  383. source.actorMessageSchemas = catalog.actor_message_schemas();
  384. source.actorCommandDefinitions = catalog.actor_command_definitions();
  385. source.actorBehaviorProfiles = catalog.actor_behavior_profiles();
  386. source.simulationEventDefinitions = catalog.simulation_event_definitions();
  387. source.runtimeSchemas = catalog.runtime_schemas();
  388. source.runtimeFields = catalog.runtime_fields();
  389. source.runtimeTypeDefinitions = catalog.runtime_type_definitions();
  390. source.sobjectRsats = catalog.sobject_rsats();
  391. source.sobjectRsatDescriptors = catalog.sobject_rsat_descriptors();
  392. source.entityTypeDefinitions = catalog.entity_type_definitions();
  393. source.sobjectRsatFieldBindings = catalog.sobject_rsat_field_bindings();
  394. source.actorStateNames = catalog.actor_state_names();
  395. source.actorSequenceTables = catalog.actor_sequence_tables();
  396. source.actorSequenceEntries = catalog.actor_sequence_entries();
  397. source.actorSequenceBindings = catalog.actor_sequence_bindings();
  398. source.authoredSceneEventKeys = catalog.authored_scene_event_keys();
  399. Bundle expected{};
  400. if (!internal::render_contract_files(source, expected)) {
  401. return false;
  402. }
  403. core::path::Buffer lua{};
  404. core::path::Buffer manifest{};
  405. return child_path(sdkDirectory, kLuaSuffix, lua)
  406. && child_path(lua.chars.data(), L"\\manifest.json", manifest)
  407. && file_equals(manifest.chars.data(), expected.manifestJson);
  408. } catch (...) {
  409. return false;
  410. }
  411. }
  412. } // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts