Files
BXSSP_Andriod/Plugins/CesiumForUnreal/Source/CesiumEditor/Private/CesiumIonSession.cpp

630 lines
20 KiB
C++
Raw Normal View History

2025-10-14 11:14:54 +08:00
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumIonSession.h"
#include "CesiumEditor.h"
#include "CesiumEditorSettings.h"
#include "CesiumIonServer.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumSourceControl.h"
#include "CesiumUtility/Uri.h"
#include "FileHelpers.h"
#include "HAL/PlatformProcess.h"
#include "Misc/App.h"
using namespace CesiumAsync;
using namespace CesiumIonClient;
namespace {
template <typename T> void logResponseErrors(const Response<T>& response) {
if (!response.errorCode.empty() && !response.errorMessage.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("%s (Code %s)"),
UTF8_TO_TCHAR(response.errorMessage.c_str()),
UTF8_TO_TCHAR(response.errorCode.c_str()));
} else if (!response.errorCode.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Code %s"),
UTF8_TO_TCHAR(response.errorCode.c_str()));
} else if (!response.errorMessage.empty()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("%s"),
UTF8_TO_TCHAR(response.errorMessage.c_str()));
}
}
void logResponseErrors(const std::exception& exception) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Exception: %s"),
UTF8_TO_TCHAR(exception.what()));
}
} // namespace
CesiumIonSession::CesiumIonSession(
CesiumAsync::AsyncSystem& asyncSystem,
const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor,
TWeakObjectPtr<UCesiumIonServer> pServer)
: _asyncSystem(asyncSystem),
_pAssetAccessor(pAssetAccessor),
_pServer(pServer),
_connection(std::nullopt),
_profile(std::nullopt),
_assets(std::nullopt),
_tokens(std::nullopt),
_defaults(std::nullopt),
_appData(std::nullopt),
_isConnecting(false),
_isResuming(false),
_isLoadingProfile(false),
_isLoadingAssets(false),
_isLoadingTokens(false),
_isLoadingDefaults(false),
_loadProfileQueued(false),
_loadAssetsQueued(false),
_loadTokensQueued(false),
_loadDefaultsQueued(false),
_authorizeUrl() {}
bool CesiumIonSession::isAuthenticationRequired() const {
return this->_appData.has_value() ? this->_appData->needsOauthAuthentication()
: true;
}
void CesiumIonSession::connect() {
if (!this->_pServer.IsValid() || this->isConnecting() ||
this->isConnected() || this->isResuming()) {
return;
}
UCesiumIonServer* pServer = this->_pServer.Get();
this->_isConnecting = true;
std::string ionServerUrl = TCHAR_TO_UTF8(*pServer->ServerUrl);
Future<std::optional<std::string>> futureApiUrl =
!pServer->ApiUrl.IsEmpty()
? this->_asyncSystem.createResolvedFuture<std::optional<std::string>>(
TCHAR_TO_UTF8(*pServer->ApiUrl))
: Connection::getApiUrl(
this->_asyncSystem,
this->_pAssetAccessor,
ionServerUrl);
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
std::move(futureApiUrl)
.thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer](
std::optional<std::string>&& ionApiUrl) {
CesiumAsync::Promise<bool> promise =
thiz->_asyncSystem.createPromise<bool>();
if (!pServer.IsValid()) {
promise.reject(
std::runtime_error("CesiumIonServer unexpectedly nullptr"));
return promise.getFuture();
}
if (!ionApiUrl) {
promise.reject(std::runtime_error(fmt::format(
"Failed to retrieve API URL from the config.json file at the "
"specified Ion server URL: {}",
ionServerUrl)));
return promise.getFuture();
}
if (pServer->ApiUrl.IsEmpty()) {
pServer->ApiUrl = UTF8_TO_TCHAR(ionApiUrl->c_str());
pServer->Modify();
UEditorLoadingAndSavingUtils::SavePackages(
{pServer->GetPackage()},
true);
}
// Make request to /appData to learn the server's authentication mode
return thiz->ensureAppDataLoaded();
})
.thenInMainThread(
[ionServerUrl, thiz, pServer = this->_pServer](bool loadedAppData) {
if (!loadedAppData || !thiz->_appData.has_value()) {
Promise<Connection> promise =
thiz->_asyncSystem.createPromise<Connection>();
promise.reject(std::runtime_error(
"Failed to obtain _appData, can't create connection"));
return promise.getFuture();
}
if (thiz->_appData->needsOauthAuthentication()) {
int64_t clientID = pServer->OAuth2ApplicationID;
return CesiumIonClient::Connection::authorize(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
"Cesium for Unreal",
clientID,
"/cesium-for-unreal/oauth2/callback",
{"assets:list",
"assets:read",
"profile:read",
"tokens:read",
"tokens:write",
"geocode"},
[thiz](const std::string& url) {
thiz->_authorizeUrl = url;
thiz->_redirectUrl =
CesiumUtility::Uri::getQueryValue(url, "redirect_uri");
FPlatformProcess::LaunchURL(
UTF8_TO_TCHAR(thiz->_authorizeUrl.c_str()),
NULL,
NULL);
},
thiz->_appData.value(),
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)),
CesiumUtility::Uri::resolve(ionServerUrl, "oauth"));
}
return thiz->_asyncSystem
.createResolvedFuture<CesiumIonClient::Connection>(
CesiumIonClient::Connection(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
"",
thiz->_appData.value(),
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl))));
})
.thenInMainThread([ionServerUrl, thiz, pServer = this->_pServer](
CesiumIonClient::Connection&& connection) {
thiz->_isConnecting = false;
thiz->_connection = std::move(connection);
UCesiumEditorSettings* pSettings =
GetMutableDefault<UCesiumEditorSettings>();
pSettings->UserAccessTokenMap.Add(
thiz->_pServer.Get(),
UTF8_TO_TCHAR(thiz->_connection.value().getAccessToken().c_str()));
pSettings->Save();
thiz->ConnectionUpdated.Broadcast();
thiz->startQueuedLoads();
})
.catchInMainThread(
[ionServerUrl, thiz, pServer = this->_pServer](std::exception&& e) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Error connecting: %s"),
UTF8_TO_TCHAR(e.what()));
thiz->_isConnecting = false;
thiz->_connection = std::nullopt;
thiz->ConnectionUpdated.Broadcast();
});
}
void CesiumIonSession::resume() {
if (!this->_pServer.IsValid() || this->isConnecting() ||
this->isConnected() || this->isResuming()) {
return;
}
const UCesiumEditorSettings* pSettings = GetDefault<UCesiumEditorSettings>();
const FString* pUserAccessToken =
pSettings->UserAccessTokenMap.Find(this->_pServer.Get());
if (!pUserAccessToken || pUserAccessToken->IsEmpty()) {
// No existing session to resume.
return;
}
this->_isResuming = true;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
// Verify that the connection actually works.
this->ensureAppDataLoaded()
.thenInMainThread([thiz, pUserAccessToken](bool loadedAppData) {
if (!loadedAppData || !thiz->_appData.has_value()) {
Promise<void> promise = thiz->_asyncSystem.createPromise<void>();
promise.reject(std::runtime_error(
"Failed to obtain _appData, can't resume connection"));
return promise.getFuture();
}
std::shared_ptr<Connection> pConnection = std::make_shared<Connection>(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
TCHAR_TO_UTF8(**pUserAccessToken),
*thiz->_appData,
TCHAR_TO_UTF8(*thiz->_pServer->ApiUrl));
return pConnection->me().thenInMainThread(
[thiz, pConnection](Response<Profile>&& response) {
logResponseErrors(response);
if (response.value.has_value()) {
thiz->_connection = std::move(*pConnection);
}
thiz->_isResuming = false;
thiz->ConnectionUpdated.Broadcast();
thiz->startQueuedLoads();
});
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isResuming = false;
});
}
void CesiumIonSession::disconnect() {
this->_connection.reset();
this->_profile.reset();
this->_assets.reset();
this->_tokens.reset();
this->_defaults.reset();
this->_appData.reset();
UCesiumEditorSettings* pSettings = GetMutableDefault<UCesiumEditorSettings>();
pSettings->UserAccessTokenMap.Remove(this->_pServer.Get());
pSettings->Save();
this->ConnectionUpdated.Broadcast();
this->ProfileUpdated.Broadcast();
this->AssetsUpdated.Broadcast();
this->TokensUpdated.Broadcast();
this->DefaultsUpdated.Broadcast();
}
void CesiumIonSession::refreshProfile() {
if (!this->_connection || this->_isLoadingProfile) {
this->_loadProfileQueued = true;
return;
}
this->_isLoadingProfile = true;
this->_loadProfileQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->me()
.thenInMainThread([thiz](Response<Profile>&& profile) {
logResponseErrors(profile);
thiz->_isLoadingProfile = false;
thiz->_profile = std::move(profile.value);
thiz->ProfileUpdated.Broadcast();
if (thiz->_loadProfileQueued)
thiz->refreshProfile();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingProfile = false;
thiz->_profile = std::nullopt;
thiz->ProfileUpdated.Broadcast();
if (thiz->_loadProfileQueued)
thiz->refreshProfile();
});
}
void CesiumIonSession::refreshAssets() {
if (!this->_connection || this->_isLoadingAssets) {
this->_loadAssetsQueued = true;
return;
}
this->_isLoadingAssets = true;
this->_loadAssetsQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->assets()
.thenInMainThread([thiz](Response<Assets>&& assets) {
logResponseErrors(assets);
thiz->_isLoadingAssets = false;
thiz->_assets = std::move(assets.value);
thiz->AssetsUpdated.Broadcast();
if (thiz->_loadAssetsQueued)
thiz->refreshAssets();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingAssets = false;
thiz->_assets = std::nullopt;
thiz->AssetsUpdated.Broadcast();
if (thiz->_loadAssetsQueued)
thiz->refreshAssets();
});
}
void CesiumIonSession::refreshTokens() {
if (!this->_connection || this->_isLoadingTokens) {
this->_loadTokensQueued = true;
return;
}
this->_isLoadingTokens = true;
this->_loadTokensQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->tokens()
.thenInMainThread([thiz](Response<TokenList>&& tokens) {
logResponseErrors(tokens);
thiz->_isLoadingTokens = false;
thiz->_tokens = tokens.value
? std::make_optional(std::move(tokens.value->items))
: std::nullopt;
thiz->TokensUpdated.Broadcast();
if (thiz->_loadTokensQueued)
thiz->refreshTokens();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingTokens = false;
thiz->_tokens = std::nullopt;
thiz->TokensUpdated.Broadcast();
if (thiz->_loadTokensQueued)
thiz->refreshTokens();
});
}
void CesiumIonSession::refreshDefaults() {
if (!this->_connection || this->_isLoadingDefaults) {
this->_loadDefaultsQueued = true;
return;
}
this->_isLoadingDefaults = true;
this->_loadDefaultsQueued = false;
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
this->_connection->defaults()
.thenInMainThread([thiz](Response<Defaults>&& defaults) {
logResponseErrors(defaults);
thiz->_isLoadingDefaults = false;
thiz->_defaults = std::move(defaults.value);
thiz->DefaultsUpdated.Broadcast();
if (thiz->_loadDefaultsQueued)
thiz->refreshDefaults();
})
.catchInMainThread([thiz](std::exception&& e) {
logResponseErrors(e);
thiz->_isLoadingDefaults = false;
thiz->_defaults = std::nullopt;
thiz->DefaultsUpdated.Broadcast();
if (thiz->_loadDefaultsQueued)
thiz->refreshDefaults();
});
}
const std::optional<CesiumIonClient::Connection>&
CesiumIonSession::getConnection() const {
return this->_connection;
}
const CesiumIonClient::Profile& CesiumIonSession::getProfile() {
static const CesiumIonClient::Profile empty{};
if (this->_profile) {
return *this->_profile;
} else {
this->refreshProfile();
return empty;
}
}
const CesiumIonClient::Assets& CesiumIonSession::getAssets() {
static const CesiumIonClient::Assets empty;
if (this->_assets) {
return *this->_assets;
} else {
this->refreshAssets();
return empty;
}
}
const std::vector<CesiumIonClient::Token>& CesiumIonSession::getTokens() {
static const std::vector<CesiumIonClient::Token> empty;
if (this->_tokens) {
return *this->_tokens;
} else {
this->refreshTokens();
return empty;
}
}
const CesiumIonClient::Defaults& CesiumIonSession::getDefaults() {
static const CesiumIonClient::Defaults empty;
if (this->_defaults) {
return *this->_defaults;
} else {
this->refreshDefaults();
return empty;
}
}
const CesiumIonClient::ApplicationData& CesiumIonSession::getAppData() {
static const CesiumIonClient::ApplicationData empty{};
if (this->_appData) {
return *this->_appData;
}
return empty;
}
bool CesiumIonSession::refreshProfileIfNeeded() {
if (this->_loadProfileQueued || !this->_profile.has_value()) {
this->refreshProfile();
}
return this->isProfileLoaded();
}
bool CesiumIonSession::refreshAssetsIfNeeded() {
if (this->_loadAssetsQueued || !this->_assets.has_value()) {
this->refreshAssets();
}
return this->isAssetListLoaded();
}
bool CesiumIonSession::refreshTokensIfNeeded() {
if (this->_loadTokensQueued || !this->_tokens.has_value()) {
this->refreshTokens();
}
return this->isTokenListLoaded();
}
bool CesiumIonSession::refreshDefaultsIfNeeded() {
if (this->_loadDefaultsQueued || !this->_defaults.has_value()) {
this->refreshDefaults();
}
return this->isDefaultsLoaded();
}
Future<Response<Token>>
CesiumIonSession::findToken(const FString& token) const {
if (!this->_connection) {
return this->getAsyncSystem().createResolvedFuture(
Response<Token>(0, "NOTCONNECTED", "Not connected to Cesium ion."));
}
std::string tokenString = TCHAR_TO_UTF8(*token);
std::optional<std::string> maybeTokenID =
Connection::getIdFromToken(tokenString);
if (!maybeTokenID) {
return this->getAsyncSystem().createResolvedFuture(
Response<Token>(0, "INVALIDTOKEN", "The token is not valid."));
}
return this->_connection->token(*maybeTokenID);
}
namespace {
Token tokenFromServer(UCesiumIonServer* pServer) {
Token result;
if (pServer) {
result.token = TCHAR_TO_UTF8(*pServer->DefaultIonAccessToken);
}
return result;
}
Future<Token> getTokenFuture(const CesiumIonSession& session) {
std::shared_ptr<const CesiumIonSession> pSession = session.shared_from_this();
TWeakObjectPtr<UCesiumIonServer> pServer = session.getServer();
if (pServer.IsValid() && !pServer->DefaultIonAccessTokenId.IsEmpty()) {
return session.getConnection()
->token(TCHAR_TO_UTF8(*pServer->DefaultIonAccessTokenId))
.thenImmediately([pServer](Response<Token>&& tokenResponse) {
if (tokenResponse.value) {
return *tokenResponse.value;
} else {
return tokenFromServer(pServer.Get());
}
});
} else if (!pServer->DefaultIonAccessToken.IsEmpty()) {
return session.findToken(pServer->DefaultIonAccessToken)
.thenImmediately([pServer](Response<Token>&& response) {
if (response.value) {
return *response.value;
} else {
return tokenFromServer(pServer.Get());
}
});
} else {
return session.getAsyncSystem().createResolvedFuture(
tokenFromServer(pServer.Get()));
}
}
} // namespace
SharedFuture<Token> CesiumIonSession::getProjectDefaultTokenDetails() {
if (this->_projectDefaultTokenDetailsFuture) {
// If the future is resolved but its token doesn't match the designated
// default token, do the request again because the user probably specified a
// new token.
if (this->_projectDefaultTokenDetailsFuture->isReady() &&
this->_projectDefaultTokenDetailsFuture->wait().token !=
TCHAR_TO_UTF8(*this->_pServer->DefaultIonAccessToken)) {
this->_projectDefaultTokenDetailsFuture.reset();
} else {
return *this->_projectDefaultTokenDetailsFuture;
}
}
if (!this->isConnected()) {
return this->getAsyncSystem()
.createResolvedFuture(tokenFromServer(this->_pServer.Get()))
.share();
}
this->_projectDefaultTokenDetailsFuture = getTokenFuture(*this).share();
return *this->_projectDefaultTokenDetailsFuture;
}
void CesiumIonSession::invalidateProjectDefaultTokenDetails() {
this->_projectDefaultTokenDetailsFuture.reset();
}
void CesiumIonSession::startQueuedLoads() {
if (this->_loadProfileQueued)
this->refreshProfile();
if (this->_loadAssetsQueued)
this->refreshAssets();
if (this->_loadTokensQueued)
this->refreshTokens();
if (this->_loadDefaultsQueued)
this->refreshDefaults();
}
CesiumAsync::Future<bool> CesiumIonSession::ensureAppDataLoaded() {
UCesiumIonServer* pServer = this->_pServer.Get();
std::shared_ptr<CesiumIonSession> thiz = this->shared_from_this();
return CesiumIonClient::Connection::appData(
thiz->_asyncSystem,
thiz->_pAssetAccessor,
std::string(TCHAR_TO_UTF8(*pServer->ApiUrl)))
.thenInMainThread(
[thiz, pServer = this->_pServer](
CesiumIonClient::Response<CesiumIonClient::ApplicationData>&&
appData) {
CesiumAsync::Promise<bool> promise =
thiz->_asyncSystem.createPromise<bool>();
thiz->_appData = appData.value;
if (!appData.value.has_value()) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Failed to obtain ion server application data: %s"),
UTF8_TO_TCHAR(appData.errorMessage.c_str()));
promise.resolve(false);
} else {
promise.resolve(true);
}
return promise.getFuture();
})
.catchInMainThread([thiz, pServer = this->_pServer](std::exception&& e) {
UE_LOG(
LogCesiumEditor,
Error,
TEXT("Error obtaining appData: %s"),
UTF8_TO_TCHAR(e.what()));
return thiz->_asyncSystem.createResolvedFuture(false);
});
}