| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- #include <Windows.h>
- #include <algorithm>
- #include <array>
- #include <atomic>
- #include <cctype>
- #include <limits>
- #include <span>
- #include <string>
- #include <string_view>
- #include <vector>
- #include "../../../core/filesystem/path.h"
- #include "../../../core/filesystem/temporary_sibling.h"
- #include "../../../state/activity_sdk/runtime.h"
- #include "activity_sdk_lua_artifacts.h"
- #include "activity_sdk_lua_artifacts_internal.h"
- namespace sunrise::client::content::activity::sdk_generation::lua_artifacts {
- namespace {
- // Published Lua tree layout below the artifact root.
- constexpr std::wstring_view kLuaSuffix = L"\\lua";
- constexpr std::wstring_view kActivitiesSuffix = L"\\activities";
- constexpr std::wstring_view kMissionsSuffix = L"\\missions";
- constexpr std::wstring_view kSunriseSuffix = L"\\sunrise";
- // The mission API schema sits beside activity_sdk.lua.
- constexpr std::wstring_view kMissionApiName = L"\\mission_api.json";
- // Files every estate publishes beside its modules: two indexes, the manifest, the SDK module,
- // the behavior module and the mission API schema.
- constexpr std::size_t kFixedFileCount = 6;
- // Distinct temporary names tried before publication fails.
- constexpr std::size_t kTemporaryAttempts = 16;
- /** One small handle owner keeps failed publication paths closed. */
- class FileHandle final {
- public:
- explicit FileHandle(HANDLE value) noexcept : value_(value) {}
- FileHandle(const FileHandle&) = delete;
- FileHandle& operator=(const FileHandle&) = delete;
- ~FileHandle() noexcept {
- if (value_ != INVALID_HANDLE_VALUE) {
- (void)CloseHandle(value_);
- }
- }
- [[nodiscard]] HANDLE get() const noexcept {
- return value_;
- }
- [[nodiscard]] bool close() noexcept {
- if (value_ == INVALID_HANDLE_VALUE) {
- return true;
- }
- const HANDLE value = value_;
- value_ = INVALID_HANDLE_VALUE;
- return CloseHandle(value) != FALSE;
- }
- private:
- HANDLE value_{INVALID_HANDLE_VALUE};
- };
- [[nodiscard]] bool ensure_directory(const wchar_t* path) noexcept {
- if (CreateDirectoryW(path, nullptr) != FALSE) {
- return true;
- }
- if (GetLastError() != ERROR_ALREADY_EXISTS) {
- return false;
- }
- const DWORD attributes = GetFileAttributesW(path);
- return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
- }
- [[nodiscard]] bool child_path(std::wstring_view parent,
- std::wstring_view suffix,
- core::path::Buffer& output) noexcept {
- return core::path::assign(output, parent) && core::path::append(output, suffix);
- }
- /** Writes the complete source in DWORD-sized chunks and rejects short writes. */
- [[nodiscard]] bool write_all(HANDLE file, std::string_view source) noexcept {
- while (!source.empty()) {
- const std::size_t count = (std::min)(
- source.size(), static_cast<std::size_t>((std::numeric_limits<DWORD>::max)()));
- DWORD transferred = 0;
- if (WriteFile(file, source.data(), static_cast<DWORD>(count), &transferred, nullptr)
- == FALSE
- || transferred != count) {
- return false;
- }
- source.remove_prefix(count);
- }
- return true;
- }
- /** Publishes only a completely written, closed sibling file and removes it after failure. */
- [[nodiscard]] bool publish_file(const wchar_t* finalPath, std::string_view source) noexcept {
- core::path::remove_stale_siblings(finalPath);
- for (std::size_t attempt = 0; attempt < kTemporaryAttempts; ++attempt) {
- core::path::Buffer temporary{};
- if (!core::path::temporary_sibling(finalPath, temporary)) {
- return false;
- }
- const HANDLE raw = CreateFileW(temporary.chars.data(),
- GENERIC_WRITE,
- 0,
- nullptr,
- CREATE_NEW,
- FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
- nullptr);
- if (raw == INVALID_HANDLE_VALUE) {
- const DWORD error = GetLastError();
- if (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS) {
- continue;
- }
- return false;
- }
- FileHandle file(raw);
- bool complete = write_all(file.get(), source);
- complete = file.close() && complete;
- if (!complete || !core::path::publish_sibling(temporary.chars.data(), finalPath)) {
- (void)DeleteFileW(temporary.chars.data());
- return false;
- }
- return true;
- }
- return false;
- }
- [[nodiscard]] bool add_bytes(std::uint64_t& total, std::string_view source) noexcept {
- if (source.size() > (std::numeric_limits<std::uint64_t>::max)() - total) {
- return false;
- }
- total += source.size();
- return true;
- }
- /** Reads one ordinary file only when its byte count matches the expected commit record. */
- [[nodiscard]] bool file_equals(const wchar_t* path, std::string_view expected) noexcept {
- if (path == nullptr || expected.size() > (std::numeric_limits<DWORD>::max)()) {
- return false;
- }
- const DWORD attributes = GetFileAttributesW(path);
- if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
- return false;
- }
- const HANDLE file = CreateFileW(path,
- GENERIC_READ,
- FILE_SHARE_READ | FILE_SHARE_DELETE,
- nullptr,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
- nullptr);
- if (file == INVALID_HANDLE_VALUE) {
- return false;
- }
- LARGE_INTEGER size{};
- std::string actual;
- bool complete = GetFileSizeEx(file, &size) != FALSE && size.QuadPart >= 0
- && static_cast<std::uint64_t>(size.QuadPart) == expected.size();
- if (complete) {
- try {
- actual.resize(expected.size());
- } catch (...) {
- complete = false;
- }
- }
- DWORD read = 0;
- if (complete) {
- complete = ReadFile(file, actual.data(), static_cast<DWORD>(actual.size()), &read, nullptr)
- != FALSE
- && read == actual.size();
- }
- complete = CloseHandle(file) != FALSE && complete;
- return complete && actual == expected;
- }
- /** Copies one catalog digest into the fixed generator source field. */
- [[nodiscard]] bool copy_digest(std::span<const std::byte> input,
- std::array<std::byte, 32>& output) noexcept {
- if (input.size() != output.size()) {
- return false;
- }
- std::copy(input.begin(), input.end(), output.begin());
- return true;
- }
- [[nodiscard]] bool publish_named(std::wstring_view directory,
- std::wstring_view name,
- std::string_view source,
- std::uint64_t& bytes) noexcept {
- core::path::Buffer path{};
- return child_path(directory, name, path) && publish_file(path.chars.data(), source)
- && add_bytes(bytes, source);
- }
- /** Converts one generator-owned ASCII stem to a child Lua filename. */
- [[nodiscard]] bool module_name(const SourceModule& module, std::wstring& output) {
- if (module.stem.empty()) {
- return false;
- }
- output.clear();
- output.reserve(module.stem.size() + 5U);
- output.push_back(L'\\');
- for (const unsigned char byte : module.stem) {
- if (byte >= 0x80U || (std::isalnum(byte) == 0 && byte != '_')) {
- return false;
- }
- output.push_back(static_cast<wchar_t>(byte));
- }
- output.append(L".lua");
- return true;
- }
- /** Shared immutable module spans and atomic scheduling state for one file batch. */
- struct ModulePublishBatch final {
- std::wstring_view activityDirectory{};
- std::wstring_view missionDirectory{};
- std::span<const SourceModule> activities{};
- std::span<const SourceModule> missions{};
- std::atomic_size_t next{};
- std::atomic_uint64_t bytes{};
- std::atomic_bool failed{};
- };
- /** Uses most cores while leaving two for the running game and server. */
- [[nodiscard]] std::size_t publish_worker_count(std::size_t files) noexcept {
- SYSTEM_INFO info{};
- GetSystemInfo(&info);
- const std::size_t processors = static_cast<std::size_t>(info.dwNumberOfProcessors);
- const std::size_t available = processors > 2U ? processors - 2U : 1U;
- return (std::min)(files, (std::min)(available, static_cast<std::size_t>(12)));
- }
- /** Publishes distinct module paths without serializing independent file writes. */
- void run_module_publish_worker(ModulePublishBatch& batch) noexcept {
- std::wstring fileName;
- std::uint64_t bytes = 0;
- const std::size_t total = batch.activities.size() + batch.missions.size();
- while (!batch.failed.load(std::memory_order_relaxed)) {
- const std::size_t index = batch.next.fetch_add(1U);
- if (index >= total) {
- break;
- }
- const bool activity = index < batch.activities.size();
- const SourceModule& module =
- activity ? batch.activities[index] : batch.missions[index - batch.activities.size()];
- const std::wstring_view directory =
- activity ? batch.activityDirectory : batch.missionDirectory;
- if (!module_name(module, fileName)
- || !publish_named(directory, fileName, module.source, bytes)) {
- batch.failed.store(true, std::memory_order_relaxed);
- break;
- }
- }
- batch.bytes.fetch_add(bytes, std::memory_order_relaxed);
- }
- /** Adapts one file publisher to the Windows thread ABI. */
- DWORD WINAPI module_publish_thread_main(void* opaque) noexcept {
- run_module_publish_worker(*static_cast<ModulePublishBatch*>(opaque));
- return 0;
- }
- /** Publishes all generated modules in parallel and returns their byte count. */
- [[nodiscard]] bool publish_modules(std::wstring_view activityDirectory,
- std::wstring_view missionDirectory,
- const Bundle& bundle,
- std::uint64_t& bytes) {
- ModulePublishBatch batch{};
- batch.activityDirectory = activityDirectory;
- batch.missionDirectory = missionDirectory;
- batch.activities = bundle.activityModules;
- batch.missions = bundle.missionModules;
- const std::size_t total = batch.activities.size() + batch.missions.size();
- const std::size_t workers = publish_worker_count(total);
- if (workers == 0) {
- return false;
- }
- std::vector<HANDLE> threads;
- threads.reserve(workers - 1U);
- for (std::size_t worker = 1; worker < workers; ++worker) {
- const HANDLE thread =
- CreateThread(nullptr, 0, &module_publish_thread_main, &batch, 0, nullptr);
- if (thread != nullptr) {
- threads.push_back(thread);
- }
- }
- run_module_publish_worker(batch);
- for (const HANDLE thread : threads) {
- (void)WaitForSingleObject(thread, INFINITE);
- (void)CloseHandle(thread);
- }
- if (batch.failed.load(std::memory_order_relaxed)) {
- return false;
- }
- const std::uint64_t moduleBytes = batch.bytes.load(std::memory_order_relaxed);
- if (moduleBytes > (std::numeric_limits<std::uint64_t>::max)() - bytes) {
- return false;
- }
- bytes += moduleBytes;
- return true;
- }
- } // namespace
- /** Publishes each file atomically; a later failure can leave earlier files updated. */
- Status publish_bundle(const wchar_t* sdkDirectory, const Bundle& bundle, Result& output) noexcept {
- output = {};
- if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0' || bundle.activityIndex.empty()
- || bundle.missionIndex.empty() || bundle.activitySdkModule.empty()
- || bundle.behaviorModule.empty() || bundle.manifestJson.empty()
- || bundle.missionApiJson.empty() || bundle.activityModules.empty()
- || bundle.missionModules.empty()) {
- return Status::invalidInput;
- }
- try {
- core::path::Buffer lua{};
- core::path::Buffer activities{};
- core::path::Buffer missions{};
- core::path::Buffer sunrise{};
- if (!child_path(sdkDirectory, kLuaSuffix, lua)
- || !child_path(lua.chars.data(), kActivitiesSuffix, activities)
- || !child_path(lua.chars.data(), kMissionsSuffix, missions)
- || !child_path(lua.chars.data(), kSunriseSuffix, sunrise)
- || !ensure_directory(lua.chars.data()) || !ensure_directory(activities.chars.data())
- || !ensure_directory(missions.chars.data())
- || !ensure_directory(sunrise.chars.data())) {
- return Status::directoryFailure;
- }
- std::uint64_t bytes = 0;
- if (!publish_named(lua.chars.data(), L"\\manifest.json", bundle.manifestJson, bytes)
- || !publish_named(
- sunrise.chars.data(), L"\\activity_sdk.lua", bundle.activitySdkModule, bytes)
- || !publish_named(sunrise.chars.data(), kMissionApiName, bundle.missionApiJson, bytes)
- || !publish_named(
- sunrise.chars.data(), L"\\behaviors.lua", bundle.behaviorModule, bytes)) {
- return Status::writeFailure;
- }
- if (!publish_modules(activities.chars.data(), missions.chars.data(), bundle, bytes)) {
- return Status::writeFailure;
- }
- if (!publish_named(lua.chars.data(), L"\\missions.lua", bundle.missionIndex, bytes)
- || !publish_named(lua.chars.data(), L"\\activities.lua", bundle.activityIndex, bytes)) {
- return Status::writeFailure;
- }
- const std::size_t sourceFiles =
- bundle.activityModules.size() + bundle.missionModules.size();
- if (sourceFiles > format::kAbsentIndex - kFixedFileCount) {
- return Status::writeFailure;
- }
- output.activityCount = static_cast<std::uint32_t>(bundle.activityModules.size());
- output.fileCount = static_cast<std::uint32_t>(sourceFiles + kFixedFileCount);
- output.byteCount = bytes;
- return Status::ready;
- } catch (...) {
- output = {};
- return Status::writeFailure;
- }
- }
- /** @return True when the published Lua modules already match this catalog. */
- bool is_current(const wchar_t* sdkDirectory, const state::activity_sdk::Catalog& catalog) noexcept {
- if (sdkDirectory == nullptr || sdkDirectory[0] == L'\0') {
- return false;
- }
- try {
- Source source{};
- if (!copy_digest(catalog.sdk_build_sha256(), source.sdkBuildSha256)
- || !copy_digest(catalog.payload_sha256(), source.sdkPayloadSha256)
- || !copy_digest(catalog.content_key_sha256(), source.contentKeySha256)
- || !copy_digest(catalog.logical_ir_sha256(), source.logicalIrSha256)) {
- return false;
- }
- source.activities = catalog.activities();
- source.states = catalog.states();
- source.slots = catalog.slots();
- source.squads = catalog.squads();
- source.authoredSceneResources = catalog.authored_scene_resources();
- source.authoredSceneSquadEdges = catalog.authored_scene_squad_edges();
- source.taskTargets = catalog.task_targets();
- source.dialogueCueTexts = catalog.dialogue_cue_texts();
- source.directiveElements = catalog.directive_elements();
- source.behaviorPrograms = catalog.behavior_programs();
- source.behaviorInputs = catalog.behavior_inputs();
- source.behaviorChannelWrites = catalog.behavior_channel_writes();
- source.behaviorOwners = catalog.behavior_owners();
- source.behaviorActivityBindings = catalog.behavior_activity_bindings();
- source.actorClasses = catalog.actor_classes();
- source.actorMessageSchemas = catalog.actor_message_schemas();
- source.actorCommandDefinitions = catalog.actor_command_definitions();
- source.actorBehaviorProfiles = catalog.actor_behavior_profiles();
- source.simulationEventDefinitions = catalog.simulation_event_definitions();
- source.runtimeSchemas = catalog.runtime_schemas();
- source.runtimeFields = catalog.runtime_fields();
- source.runtimeTypeDefinitions = catalog.runtime_type_definitions();
- source.sobjectRsats = catalog.sobject_rsats();
- source.sobjectRsatDescriptors = catalog.sobject_rsat_descriptors();
- source.entityTypeDefinitions = catalog.entity_type_definitions();
- source.sobjectRsatFieldBindings = catalog.sobject_rsat_field_bindings();
- source.actorStateNames = catalog.actor_state_names();
- source.actorSequenceTables = catalog.actor_sequence_tables();
- source.actorSequenceEntries = catalog.actor_sequence_entries();
- source.actorSequenceBindings = catalog.actor_sequence_bindings();
- source.authoredSceneEventKeys = catalog.authored_scene_event_keys();
- Bundle expected{};
- if (!internal::render_contract_files(source, expected)) {
- return false;
- }
- core::path::Buffer lua{};
- core::path::Buffer manifest{};
- core::path::Buffer sunrise{};
- core::path::Buffer missionApi{};
- return child_path(sdkDirectory, kLuaSuffix, lua)
- && child_path(lua.chars.data(), L"\\manifest.json", manifest)
- && file_equals(manifest.chars.data(), expected.manifestJson)
- && child_path(lua.chars.data(), kSunriseSuffix, sunrise)
- && child_path(sunrise.chars.data(), kMissionApiName, missionApi)
- && file_equals(missionApi.chars.data(), expected.missionApiJson);
- } catch (...) {
- return false;
- }
- }
- } // namespace sunrise::client::content::activity::sdk_generation::lua_artifacts
|