516 lines
15 KiB
C++

// @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<FString> GetFileNames() const;
bool TryReadFile(FStringView FileName, TArray<uint8>& OutData) const;
private:
TMap<FString, zip_int64_t> 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<FString> FZipArchiveReader::GetFileNames() const
{
TArray<FString> Result;
EmbeddedFileToIndex.GenerateKeyArray(Result);
return Result;
}
bool FZipArchiveReader::TryReadFile(FStringView FileName, TArray<uint8>& 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<FZipArchiveReader*>(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<zip_uint64_t>(FileSize - FilePos), DataLen);
if (!FileHandle->Read(reinterpret_cast<uint8*>(OutData), DataLen))
{
return 0;
}
FilePos += DataLen;
return DataLen;
case ZIP_SOURCE_CLOSE:
return 0;
case ZIP_SOURCE_STAT:
{
zip_stat_t* OutStat = reinterpret_cast<zip_stat_t*>(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<zip_uint32_t*>(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<uint64>(NewOffset))
{
return -1;
}
if (!FileHandle->Seek(NewOffset))
{
return -1;
}
FilePos = NewOffset;
return 0;
}
case ZIP_SOURCE_TELL:
return static_cast<zip_int64_t>(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<SNotificationItem>& Notification)
{
IPlatformFile& FileManager = FPlatformFileManager::Get().GetPlatformFile();
IFileHandle* ArchiveFileHandle = FileManager.OpenRead(*Path);
const FZipArchiveReader ZipArchiveReader(ArchiveFileHandle);
if (!ZipArchiveReader.IsValid())
{
return false;
}
const TArray<FString> 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<uint8> 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<OculusXRTelemetry::Events::FSimulator> 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<void()>& 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<SNotificationItem> 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<IHttpRequest, ESPMode::ThreadSafe> 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<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("OculusXR"));
if (Plugin.IsValid())
{
FString VersionName = Plugin->GetDescriptor().VersionName;
TArray<FString> 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<SNotificationItem>& Notification,
const TFunction<void()>& 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