activity_sdk_lua_publish.cpp 18 KB

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