// @lint-ignore-every LICENSELINT // Copyright Epic Games, Inc. All Rights Reserved. #include "OculusXRSimulator.h" #if PLATFORM_WINDOWS #include "HttpModule.h" #include "Interfaces/IHttpResponse.h" #include "Interfaces/IHttpRequest.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "HAL/PlatformFilemanager.h" #include "GenericPlatform/GenericPlatformFile.h" #include "libzip/zip.h" #include "HAL/FileManager.h" #include "OculusXRHMDRuntimeSettings.h" #include "OculusXRTelemetryEvents.h" #include "Misc/MessageDialog.h" #include "OpenXR/OculusXROpenXRUtilities.h" #include "Internationalization/Regex.h" #include "Windows/WindowsPlatformMisc.h" #include "Interfaces/IPluginManager.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #if WITH_EDITOR #include "UnrealEdMisc.h" #endif // WITH_EDITOR const FString OpenXrRuntimeEnvKey = "XR_RUNTIME_JSON"; const FString PreviousOpenXrRuntimeEnvKey = "XR_RUNTIME_JSON_PREV"; namespace { class FZipArchiveReader { public: FZipArchiveReader(IFileHandle* InFileHandle); ~FZipArchiveReader(); bool IsValid() const; TArray GetFileNames() const; bool TryReadFile(FStringView FileName, TArray& OutData) const; private: TMap EmbeddedFileToIndex; IFileHandle* FileHandle = nullptr; zip_source_t* ZipFileSource = nullptr; zip_t* ZipFile = nullptr; uint64 FilePos = 0; uint64 FileSize = 0; void Destruct(); zip_int64_t ZipSourceFunctionReader(void* OutData, zip_uint64_t DataLen, zip_source_cmd_t Command); static zip_int64_t ZipSourceFunctionReaderStatic(void* InUserData, void* OutData, zip_uint64_t DataLen, zip_source_cmd_t Command); }; FZipArchiveReader::FZipArchiveReader(IFileHandle* InFileHandle) : FileHandle(InFileHandle) { if (!FileHandle) { Destruct(); return; } if (FileHandle->Tell() != 0) { FileHandle->Seek(0); } FilePos = 0; FileSize = FileHandle->Size(); zip_error_t ZipError; zip_error_init(&ZipError); ZipFileSource = zip_source_function_create(ZipSourceFunctionReaderStatic, this, &ZipError); if (!ZipFileSource) { zip_error_fini(&ZipError); Destruct(); return; } zip_error_init(&ZipError); ZipFile = zip_open_from_source(ZipFileSource, ZIP_RDONLY, &ZipError); if (!ZipFile) { zip_error_fini(&ZipError); Destruct(); return; } zip_int64_t NumberOfFiles = zip_get_num_entries(ZipFile, 0); if (NumberOfFiles < 0 || MAX_int32 < NumberOfFiles) { Destruct(); return; } EmbeddedFileToIndex.Reserve(NumberOfFiles); // produce the manifest file first in case the operation gets canceled while unzipping for (zip_int64_t i = 0; i < NumberOfFiles; i++) { zip_stat_t ZipFileStat; if (zip_stat_index(ZipFile, i, 0, &ZipFileStat) != 0) { Destruct(); return; } zip_uint64_t ValidStat = ZipFileStat.valid; if (!(ValidStat & ZIP_STAT_NAME)) { Destruct(); return; } EmbeddedFileToIndex.Add(FString(ANSI_TO_TCHAR(ZipFileStat.name)), i); } } FZipArchiveReader::~FZipArchiveReader() { Destruct(); } void FZipArchiveReader::Destruct() { EmbeddedFileToIndex.Empty(); if (ZipFile) { zip_close(ZipFile); ZipFile = nullptr; } if (ZipFileSource) { zip_source_close(ZipFileSource); ZipFileSource = nullptr; } delete FileHandle; FileHandle = nullptr; } bool FZipArchiveReader::IsValid() const { return ZipFile != nullptr; } TArray FZipArchiveReader::GetFileNames() const { TArray Result; EmbeddedFileToIndex.GenerateKeyArray(Result); return Result; } bool FZipArchiveReader::TryReadFile(FStringView FileName, TArray& OutData) const { OutData.Reset(); const zip_int64_t* Index = EmbeddedFileToIndex.FindByHash(GetTypeHash(FileName), FileName); if (!Index) { return false; } zip_stat_t ZipFileStat; if (zip_stat_index(ZipFile, *Index, 0, &ZipFileStat) != 0) { return false; } if (!(ZipFileStat.valid & ZIP_STAT_SIZE)) { return false; } if (ZipFileStat.size == 0) { return true; } if (ZipFileStat.size > MAX_int32) { return false; } OutData.SetNumUninitialized(ZipFileStat.size, EAllowShrinking::No); zip_file* EmbeddedFile = zip_fopen_index(ZipFile, *Index, 0 /* flags */); if (!EmbeddedFile) { OutData.Reset(); return false; } bool bReadSuccess = zip_fread(EmbeddedFile, OutData.GetData(), ZipFileStat.size) == ZipFileStat.size; zip_fclose(EmbeddedFile); if (!bReadSuccess) { OutData.Reset(); return false; } return true; } zip_int64_t FZipArchiveReader::ZipSourceFunctionReaderStatic( void* InUserData, void* OutData, zip_uint64_t DataLen, zip_source_cmd_t Command) { return reinterpret_cast(InUserData)->ZipSourceFunctionReader(OutData, DataLen, Command); } zip_int64_t FZipArchiveReader::ZipSourceFunctionReader( void* OutData, zip_uint64_t DataLen, zip_source_cmd_t Command) { switch (Command) { case ZIP_SOURCE_OPEN: return 0; case ZIP_SOURCE_READ: if (FilePos == FileSize) { return 0; } DataLen = FMath::Min(static_cast(FileSize - FilePos), DataLen); if (!FileHandle->Read(reinterpret_cast(OutData), DataLen)) { return 0; } FilePos += DataLen; return DataLen; case ZIP_SOURCE_CLOSE: return 0; case ZIP_SOURCE_STAT: { zip_stat_t* OutStat = reinterpret_cast(OutData); zip_stat_init(OutStat); OutStat->size = FileSize; OutStat->comp_size = FileSize; OutStat->comp_method = ZIP_CM_STORE; OutStat->encryption_method = ZIP_EM_NONE; OutStat->valid = ZIP_STAT_SIZE | ZIP_STAT_COMP_SIZE | ZIP_STAT_COMP_METHOD | ZIP_STAT_ENCRYPTION_METHOD; return sizeof(*OutStat); } case ZIP_SOURCE_ERROR: { zip_uint32_t* OutLibZipError = reinterpret_cast(OutData); zip_uint32_t* OutSystemError = OutLibZipError + 1; *OutLibZipError = ZIP_ER_INTERNAL; *OutSystemError = 0; return 2 * sizeof(*OutLibZipError); } case ZIP_SOURCE_FREE: return 0; case ZIP_SOURCE_SEEK: { zip_int64_t NewOffset = zip_source_seek_compute_offset(FilePos, FileSize, OutData, DataLen, nullptr); if (NewOffset < 0 || FileSize < static_cast(NewOffset)) { return -1; } if (!FileHandle->Seek(NewOffset)) { return -1; } FilePos = NewOffset; return 0; } case ZIP_SOURCE_TELL: return static_cast(FilePos); case ZIP_SOURCE_SUPPORTS: return zip_source_make_command_bitmap(ZIP_SOURCE_OPEN, ZIP_SOURCE_READ, ZIP_SOURCE_CLOSE, ZIP_SOURCE_STAT, ZIP_SOURCE_ERROR, ZIP_SOURCE_FREE, ZIP_SOURCE_SEEK, ZIP_SOURCE_TELL, ZIP_SOURCE_SUPPORTS, -1); default: return 0; } } bool Unzip(const FString& Path, const FString& TargetPath, const TSharedPtr& Notification) { IPlatformFile& FileManager = FPlatformFileManager::Get().GetPlatformFile(); IFileHandle* ArchiveFileHandle = FileManager.OpenRead(*Path); const FZipArchiveReader ZipArchiveReader(ArchiveFileHandle); if (!ZipArchiveReader.IsValid()) { return false; } const TArray ArchiveFiles = ZipArchiveReader.GetFileNames(); uint64 Size = ArchiveFiles.Num(); uint64 Index = 0; for (const FString& FileName : ArchiveFiles) { Index++; if (Notification.IsValid()) { Notification->SetText(FText::FromString(FString::Format(TEXT("Unzipping {0} / {1}"), { Index, Size }))); } if (FileName.EndsWith("/") || FileName.EndsWith("\\")) continue; if (TArray FileBuffer; ZipArchiveReader.TryReadFile(FileName, FileBuffer)) { if (!FFileHelper::SaveArrayToFile(FileBuffer, *(TargetPath / FileName))) { return false; } } } return true; } } // namespace bool FMetaXRSimulator::IsSimulatorActivated() { FString MetaXRSimPath = GetSimulatorJsonPath(); FString CurRuntimePath = FWindowsPlatformMisc::GetEnvironmentVariable(*OpenXrRuntimeEnvKey); return (!MetaXRSimPath.IsEmpty() && MetaXRSimPath == CurRuntimePath); } void FMetaXRSimulator::ToggleOpenXRRuntime() { OculusXRTelemetry::TScopedMarker Event; FString MetaXRSimPath = GetSimulatorJsonPath(); if (!IFileManager::Get().FileExists(*MetaXRSimPath)) { InstallSimulator(ToggleOpenXRRuntime); UE_LOG(LogMetaXRSim, Log, TEXT("Meta XR Simulator Not Installed.\nInstalling Meta XR Simulator.")); return; } #if WITH_EDITOR if (OculusXR::IsOpenXRSystem()) { FString ActivationText = IsSimulatorActivated() ? "deactivate" : "activate"; FString Message = FString::Format(TEXT("A restart is required in order to {0} XR simulator. The restart must be performed from this dialog, opening and closing the editor manually will not work. Restart now?"), { ActivationText }); if (FMessageDialog::Open(EAppMsgType::OkCancel, FText::FromString(Message)) == EAppReturnType::Cancel) { UE_LOG(LogMetaXRSim, Log, TEXT("Meta XR Simulator %s action canceled."), *ActivationText); const auto& NotEnd = Event.SetResult(OculusXRTelemetry::EAction::Fail).AddAnnotation("reason", "restart canceled"); return; } } #endif // WITH_EDITOR if (IsSimulatorActivated()) { // Deactivate MetaXR Simulator FString PrevOpenXrRuntimeEnvKey = FWindowsPlatformMisc::GetEnvironmentVariable(*PreviousOpenXrRuntimeEnvKey); FWindowsPlatformMisc::SetEnvironmentVar(*PreviousOpenXrRuntimeEnvKey, TEXT("")); FWindowsPlatformMisc::SetEnvironmentVar(*OpenXrRuntimeEnvKey, *PrevOpenXrRuntimeEnvKey); UE_LOG(LogMetaXRSim, Log, TEXT("Meta XR Simulator is deactivated. (%s : %s)"), *OpenXrRuntimeEnvKey, *PrevOpenXrRuntimeEnvKey); const auto& NotEnd = Event.AddAnnotation("action", "deactivated"); } else { // Activate MetaXR Simulator FString CurOpenXrRuntimeEnvKey = FWindowsPlatformMisc::GetEnvironmentVariable(*OpenXrRuntimeEnvKey); FWindowsPlatformMisc::SetEnvironmentVar(*PreviousOpenXrRuntimeEnvKey, *CurOpenXrRuntimeEnvKey); FWindowsPlatformMisc::SetEnvironmentVar(*OpenXrRuntimeEnvKey, *MetaXRSimPath); UE_LOG(LogMetaXRSim, Log, TEXT("Meta XR Simulator is activated. (%s : %s)"), *OpenXrRuntimeEnvKey, *MetaXRSimPath); const auto& NotEnd = Event.AddAnnotation("action", "activated"); } #if WITH_EDITOR if (OculusXR::IsOpenXRSystem()) { FUnrealEdMisc::Get().RestartEditor(false); } #endif // WITH_EDITOR } FString FMetaXRSimulator::GetSimulatorJsonPath() { return FPaths::Combine(GetPackagePath(), TEXT("meta_openxr_simulator.json")); } bool FMetaXRSimulator::IsSimulatorInstalled() { return FPaths::FileExists(GetSimulatorJsonPath()); } void FMetaXRSimulator::TryActivateOnStartup() { #if OCULUS_HMD_SUPPORTED_PLATFORMS && WITH_EDITOR // If -HMDSimulator is used as the command option to launch UE, use simulator runtime instead of the physical HMD runtime (like PC-Link). if (FParse::Param(FCommandLine::Get(), TEXT("HMDSimulator"))) { if (IsSimulatorActivated()) { return; } ToggleOpenXRRuntime(); } #endif // OCULUS_HMD_SUPPORTED_PLATFORMS && WITH_EDITOR } FString FMetaXRSimulator::GetPackagePath() { return FPaths::Combine(FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")), TEXT("MetaXR"), TEXT("MetaXRSimulator"), GetVersion()); } void FMetaXRSimulator::InstallSimulator(const TFunction& OnSuccess) { FNotificationInfo Progress(FText::FromString("Installing Meta XR Simulator...")); Progress.bFireAndForget = false; Progress.FadeInDuration = 0.5f; Progress.FadeOutDuration = 0.5f; Progress.ExpireDuration = 5.0f; Progress.bUseThrobber = true; Progress.bUseSuccessFailIcons = true; TSharedPtr NotificationItem = FSlateNotificationManager::Get().AddNotification(Progress); if (NotificationItem.IsValid()) { NotificationItem->SetCompletionState(SNotificationItem::CS_Pending); } auto DestinationFolder = GetPackagePath(); auto DownloadPath = FPaths::Combine(FPaths::EngineSavedDir(), TEXT("Downloads"), TEXT("MetaXRSimulator"), GetVersion(), TEXT("MetaXRSimulator.zip")); if (FPaths::FileExists(DownloadPath)) { UnzipSimulator(DownloadPath, DestinationFolder, NotificationItem, OnSuccess); return; } TSharedPtr Request = FHttpModule::Get().CreateRequest(); Request->OnProcessRequestComplete().BindLambda([DownloadPath, DestinationFolder, NotificationItem, OnSuccess](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) { Request->OnRequestProgress64().Unbind(); if (Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) { // Save the downloaded zip file FFileHelper::SaveArrayToFile(Response->GetContent(), *DownloadPath); if (NotificationItem.IsValid()) { NotificationItem->SetText(FText::FromString("Unzipping ... ")); } UnzipSimulator(DownloadPath, DestinationFolder, NotificationItem, OnSuccess); return; } UE_LOG(LogMetaXRSim, Error, TEXT("Failed to install Meta XR Simulator.")); if (NotificationItem.IsValid()) { NotificationItem->SetText(FText::FromString("Installation failed!")); NotificationItem->SetCompletionState(SNotificationItem::CS_Fail); NotificationItem->ExpireAndFadeout(); } }); Request->OnRequestProgress64().BindLambda([NotificationItem](const FHttpRequestPtr& Request, uint64 /* BytesSent */, uint64 BytesReceived) { uint64 ContentLength = Request->GetResponse()->GetContentLength(); if (NotificationItem.IsValid()) { NotificationItem->SetText(FText::FromString(FString::Format(TEXT("Downloading {0} / {1}"), { BytesReceived, ContentLength }))); } }); Request->SetURL("https://www.facebook.com/horizon_devcenter_download?app_id=28549923061320041&sdk_version=" + GetVersion()); Request->SetVerb(TEXT("GET")); Request->ProcessRequest(); } FString FMetaXRSimulator::GetVersion() { TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("OculusXR")); if (Plugin.IsValid()) { FString VersionName = Plugin->GetDescriptor().VersionName; TArray ParsedParts; VersionName.ParseIntoArray(ParsedParts, TEXT("."), true); return FString::FromInt(FCString::Atoi(*ParsedParts[1]) - 32); } return "0"; } void FMetaXRSimulator::UnzipSimulator(const FString& Path, const FString& TargetPath, const TSharedPtr& Notification, const TFunction& OnSuccess) { bool bSuccess = Unzip(Path, TargetPath, Notification); if (!bSuccess || !IsSimulatorInstalled()) { UE_LOG(LogMetaXRSim, Error, TEXT("Failed to unzip the file.")); if (Notification.IsValid()) { Notification->SetText(FText::FromString("Installation failed!")); Notification->SetCompletionState(SNotificationItem::CS_Fail); Notification->ExpireAndFadeout(); } return; } if (Notification.IsValid()) { Notification->SetText(FText::FromString("Installation succeeded!")); Notification->SetCompletionState(SNotificationItem::CS_Success); Notification->ExpireAndFadeout(); } if (OnSuccess) { OnSuccess(); } } #endif // PLATFORM_WINDOWS