activity_sdk_lua_publish.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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
  145. || (attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) {
  146. return false;
  147. }
  148. const HANDLE file = CreateFileW(path,
  149. GENERIC_READ,
  150. FILE_SHARE_READ | FILE_SHARE_DELETE,
  151. nullptr,
  152. OPEN_EXISTING,
  153. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
  154. nullptr);
  155. if (file == INVALID_HANDLE_VALUE) {
  156. return false;
  157. }
  158. LARGE_INTEGER size{};
  159. std::string actual;
  160. bool complete = GetFileSizeEx(file, &size) != FALSE && size.QuadPart >= 0
  161. && static_cast<std::uint64_t>(size.QuadPart) == expected.size();
  162. if (complete) {
  163. try {
  164. actual.resize(expected.size());
  165. } catch (...) {
  166. complete = false;
  167. }
  168. }
  169. DWORD read = 0;
  170. if (complete) {
  171. complete = ReadFile(file, actual.data(), static_cast<DWORD>(actual.size()), &read, nullptr)
  172. != FALSE
  173. && read == actual.size();
  174. }
  175. complete = CloseHandle(file) != FALSE && complete;
  176. return complete && actual == expected;
  177. }
  178. /** Copies one catalog digest into the fixed generator source field. */
  179. [[nodiscard]] bool copy_digest(std::span<const std::byte> input,
  180. std::array<std::byte, 32>& output) noexcept {
  181. if (input.size() != output.size()) {
  182. return false;
  183. }
  184. std::copy(input.begin(), input.end(), output.begin());
  185. return true;
  186. }
  187. [[nodiscard]] bool publish_named(std::wstring_view directory,
  188. std::wstring_view name,
  189. std::string_view source,
  190. std::uint64_t& bytes) noexcept {
  191. core::path::Buffer path{};
  192. return child_path(directory, name, path) && publish_file(path.chars.data(), source)
  193. && add_bytes(bytes, source);
  194. }
  195. /** Converts one generator-owned ASCII stem to a child Lua filename. */
  196. [[nodiscard]] bool module_name(const SourceModule& module, std::wstring& output) {
  197. if (module.stem.empty()) {
  198. return false;
  199. }
  200. output.clear();
  201. output.reserve(module.stem.size() + 5U);
  202. output.push_back(L'\\');
  203. for (const unsigned char byte : module.stem) {
  204. if (byte >= 0x80U || (std::isalnum(byte) == 0 && byte != '_')) {
  205. return false;
  206. }
  207. output.push_back(static_cast<wchar_t>(byte));
  208. }
  209. output.append(L".lua");
  210. return true;
  211. }
  212. /** Shared immutable module spans and atomic scheduling state for one file batch. */
  213. struct ModulePublishBatch final {
  214. std::wstring_view activityDirectory{};
  215. std::wstring_view missionDirectory{};
  216. std::span<const SourceModule> activities{};
  217. std::span<const SourceModule> missions{};
  218. std::atomic_size_t next{};
  219. std::atomic_uint64_t bytes{};
  220. std::atomic_bool failed{};
  221. };
  222. /** Uses most cores while leaving two for the running game and server. */
  223. [[nodiscard]] std::size_t publish_worker_count(std::size_t files) noexcept {
  224. SYSTEM_INFO info{};
  225. GetSystemInfo(&info);
  226. const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
  227. const std::size_t available = processors > 2U ? processors - 2U : 1U;
  228. return (std::min)(files, (std::min)(available, static_cast<std::size_t>(12)));
  229. }
  230. /** Publishes distinct module paths without serializing independent file writes. */
  231. void run_module_publish_worker(ModulePublishBatch& batch) noexcept {
  232. std::wstring fileName;
  233. std::uint64_t bytes = 0;
  234. const std::size_t total = batch.activities.size() + batch.missions.size();
  235. while (!batch.failed.load(std::memory_order_relaxed)) {
  236. const std::size_t index = batch.next.fetch_add(1U);
  237. if (index >= total) {
  238. break;
  239. }
  240. const bool activity = index < batch.activities.size();
  241. const SourceModule& module =
  242. activity ? batch.activities[index] : batch.missions[index - batch.activities.size()];
  243. const std::wstring_view directory =
  244. activity ? batch.activityDirectory : batch.missionDirectory;
  245. if (!module_name(module, fileName)
  246. || !publish_named(directory, fileName, module.source, bytes)) {
  247. batch.failed.store(true, std::memory_order_relaxed);
  248. break;
  249. }
  250. }
  251. batch.bytes.fetch_add(bytes, std::memory_order_relaxed);
  252. }
  253. /** Adapts one file publisher to the Windows thread ABI. */
  254. DWORD WINAPI module_publish_thread_main(void* opaque) noexcept {
  255. run_module_publish_worker(*static_cast<ModulePublishBatch*>(opaque));
  256. return 0;
  257. }
  258. /** Publishes all generated modules in parallel and returns their byte count. */
  259. [[nodiscard]] bool publish_modules(std::wstring_view activityDirectory,
  260. std::wstring_view missionDirectory,
  261. const Bundle& bundle,
  262. std::uint64_t& bytes) {
  263. ModulePublishBatch batch{};
  264. batch.activityDirectory = activityDirectory;
  265. batch.missionDirectory = missionDirectory;
  266. batch.activities = bundle.activityModules;
  267. batch.missions = bundle.missionModules;
  268. const std::size_t total = batch.activities.size() + batch.missions.size();
  269. const std::size_t workers = publish_worker_count(total);
  270. if (workers == 0) {
  271. return false;
  272. }
  273. std::vector<HANDLE> threads;
  274. threads.reserve(workers - 1U);
  275. for (std::size_t worker = 1; worker < workers; ++worker) {
  276. const HANDLE thread =
  277. CreateThread(nullptr, 0, &module_publish_thread_main, &batch, 0, nullptr);
  278. if (thread != nullptr) {
  279. threads.push_back(thread);
  280. }
  281. }
  282. run_module_publish_worker(batch);
  283. for (const HANDLE thread : threads) {
  284. (void)WaitForSingleObject(thread, INFINITE);
  285. (void)CloseHandle(thread);
  286. }
  287. if (batch.failed.load(std::memory_order_relaxed)) {
  288. return false;
  289. }
  290. const std::uint64_t moduleBytes = batch.bytes.load(std::memory_order_relaxed);
  291. if (moduleBytes > (std::numeric_limits<std::uint64_t>::max)() - bytes) {
  292. return false;
  293. }
  294. bytes += moduleBytes;
  295. return true;
  296. }
  297. } // namespace
  298. /** Publishes each file atomically; a later failure can leave earlier files updated. */
  299. Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result& output) noexcept {
  300. output = {};
  301. if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0' || bundle.activityIndex.empty()
  302. || bundle.missionIndex.empty() || bundle.activitySdkModule.empty()
  303. || bundle.behaviorModule.empty() || bundle.manifestJson.empty()
  304. || bundle.activityModules.empty() || bundle.missionModules.empty()) {
  305. return Status::invalidInput;
  306. }
  307. try {
  308. core::path::Buffer lua{};
  309. core::path::Buffer activities{};
  310. core::path::Buffer missions{};
  311. core::path::Buffer sunrise{};
  312. if (!child_path(sdkDirectory, kLuaSuffix, lua)
  313. || !child_path(lua.chars.data(), kActivitiesSuffix, activities)
  314. || !child_path(lua.chars.data(), kMissionsSuffix, missions)
  315. || !child_path(lua.chars.data(), kSunriseSuffix, sunrise)
  316. || !ensure_directory(lua.chars.data()) || !ensure_directory(activities.chars.data())
  317. || !ensure_directory(missions.chars.data())
  318. || !ensure_directory(sunrise.chars.data())) {
  319. return Status::directoryFailure;
  320. }
  321. std::uint64_t bytes = 0;
  322. if (!publish_named(lua.chars.data(), L"\\manifest.json", bundle.manifestJson, bytes)
  323. || !publish_named(
  324. sunrise.chars.data(), L"\\activity_sdk.lua", bundle.activitySdkModule, bytes)
  325. || !publish_named(
  326. sunrise.chars.data(), L"\\behaviors.lua", bundle.behaviorModule, bytes)) {
  327. return Status::writeFailure;
  328. }
  329. if (!publish_modules(activities.chars.data(), missions.chars.data(), bundle, bytes)) {
  330. return Status::writeFailure;
  331. }
  332. if (!publish_named(lua.chars.data(), L"\\missions.lua", bundle.missionIndex, bytes)
  333. || !publish_named(lua.chars.data(), L"\\activities.lua", bundle.activityIndex, bytes)) {
  334. return Status::writeFailure;
  335. }
  336. const std::size_t sourceFiles =
  337. bundle.activityModules.size() + bundle.missionModules.size();
  338. if (sourceFiles > format::kAbsentIndex - 5U) {
  339. return Status::writeFailure;
  340. }
  341. output.activityCount = static_cast<std::uint32_t>(bundle.activityModules.size());
  342. output.fileCount = static_cast<std::uint32_t>(sourceFiles + 5U);
  343. output.byteCount = bytes;
  344. return Status::ready;
  345. } catch (...) {
  346. output = {};
  347. return Status::writeFailure;
  348. }
  349. }
  350. Status publish(const wchar_t* sdkDirectory, const Source& source, Result& output) noexcept {
  351. output = {};
  352. Bundle bundle{};
  353. const Status built = build(source, bundle);
  354. return built == Status::ready ? publish_bundle(sdkDirectory, bundle, output) : built;
  355. }
  356. /** @return True when the published Lua modules already match this catalog. */
  357. bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog& catalog) noexcept {
  358. if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0') {
  359. return false;
  360. }
  361. try {
  362. Source source{};
  363. if (!copy_digest(catalog.sdk_build_sha256(), source.sdkBuildSha256)
  364. || !copy_digest(catalog.payload_sha256(), source.sdkPayloadSha256)
  365. || !copy_digest(catalog.content_key_sha256(), source.contentKeySha256)
  366. || !copy_digest(catalog.logical_ir_sha256(), source.logicalIrSha256)) {
  367. return false;
  368. }
  369. source.activities = catalog.activities();
  370. source.states = catalog.states();
  371. source.slots = catalog.slots();
  372. source.squads = catalog.squads();
  373. source.authoredSceneResources = catalog.authored_scene_resources();
  374. source.authoredSceneSquadEdges = catalog.authored_scene_squad_edges();
  375. source.taskTargets = catalog.task_targets();
  376. source.dialogueCueTexts = catalog.dialogue_cue_texts();
  377. source.directiveElements = catalog.directive_elements();
  378. source.behaviorPrograms = catalog.behavior_programs();
  379. source.behaviorInputs = catalog.behavior_inputs();
  380. source.behaviorChannelWrites = catalog.behavior_channel_writes();
  381. source.behaviorOwners = catalog.behavior_owners();
  382. source.behaviorActivityBindings = catalog.behavior_activity_bindings();
  383. source.actorClasses = catalog.actor_classes();
  384. source.actorMessageSchemas = catalog.actor_message_schemas();
  385. source.actorCommandDefinitions = catalog.actor_command_definitions();
  386. source.actorBehaviorProfiles = catalog.actor_behavior_profiles();
  387. source.simulationEventDefinitions = catalog.simulation_event_definitions();
  388. source.runtimeSchemas = catalog.runtime_schemas();
  389. source.runtimeFields = catalog.runtime_fields();
  390. source.runtimeTypeDefinitions = catalog.runtime_type_definitions();
  391. source.sobjectRsats = catalog.sobject_rsats();
  392. source.sobjectRsatDescriptors = catalog.sobject_rsat_descriptors();
  393. source.entityTypeDefinitions = catalog.entity_type_definitions();
  394. source.sobjectRsatFieldBindings = catalog.sobject_rsat_field_bindings();
  395. source.actorStateNames = catalog.actor_state_names();
  396. Bundle expected{};
  397. if (!internal::render_contract_files(source, expected)) {
  398. return false;
  399. }
  400. core::path::Buffer lua{};
  401. core::path::Buffer manifest{};
  402. return child_path(sdkDirectory, kLuaSuffix, lua)
  403. && child_path(lua.chars.data(), L"\\manifest.json", manifest)
  404. && file_equals(manifest.chars.data(), expected.manifestJson);
  405. } catch (...) {
  406. return false;
  407. }
  408. }
  409. } // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts