bmh/FlightSimulation/Plugins/CesiumForUnreal_5.4/Source/CesiumRuntime/Private/CesiumCreditSystem.cpp
2025-02-07 22:52:32 +08:00

468 lines
14 KiB
C++

// Copyright 2020-2024 CesiumGS, Inc. and Contributors
#include "CesiumCreditSystem.h"
#include "CesiumCommon.h"
#include "CesiumCreditSystemBPLoader.h"
#include "CesiumRuntime.h"
#include "CesiumUtility/CreditSystem.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "ScreenCreditsWidget.h"
#include <string>
#include <tidybuffio.h>
#include <vector>
#if WITH_EDITOR
#include "Editor.h"
#include "EditorSupportDelegates.h"
#include "GameDelegates.h"
#include "IAssetViewport.h"
#include "LevelEditor.h"
#include "Modules/ModuleManager.h"
#endif
/*static*/ UObject* ACesiumCreditSystem::CesiumCreditSystemBP = nullptr;
namespace {
/**
* @brief Tries to find the default credit system in the given level.
*
* This will search all actors of the given level for a `ACesiumCreditSystem`
* whose name starts with `"CesiumCreditSystemDefault"` that is *valid*
* (i.e. not pending kill).
*
* @param Level The level
* @return The default credit system, or `nullptr` if there is none.
*/
ACesiumCreditSystem* findValidDefaultCreditSystem(ULevel* Level) {
if (!IsValid(Level)) {
UE_LOG(
LogCesium,
Warning,
TEXT("No valid level for findValidDefaultCreditSystem"));
return nullptr;
}
#if ENGINE_VERSION_5_4_OR_HIGHER
TArray<TObjectPtr<AActor>>& Actors = Level->Actors;
using LevelActorPointer = TObjectPtr<AActor>*;
#else
TArray<AActor*>& Actors = Level->Actors;
using LevelActorPointer = AActor**;
#endif
LevelActorPointer DefaultCreditSystemPtr =
Actors.FindByPredicate([](AActor* const& InItem) {
if (!IsValid(InItem)) {
return false;
}
if (!InItem->IsA(ACesiumCreditSystem::StaticClass())) {
return false;
}
if (!InItem->GetName().StartsWith("CesiumCreditSystemDefault")) {
return false;
}
return true;
});
if (!DefaultCreditSystemPtr) {
return nullptr;
}
AActor* DefaultCreditSystem = *DefaultCreditSystemPtr;
return Cast<ACesiumCreditSystem>(DefaultCreditSystem);
}
bool checkIfInSubLevel(ACesiumCreditSystem* pCreditSystem) {
if (pCreditSystem->GetLevel() != pCreditSystem->GetWorld()->PersistentLevel) {
UE_LOG(
LogCesium,
Warning,
TEXT(
"CesiumCreditSystem should only exist in the Persistent Level. Adding it to a sub-level may cause credits to be lost."));
return true;
} else {
return false;
}
}
} // namespace
FName ACesiumCreditSystem::DEFAULT_CREDITSYSTEM_TAG =
FName("DEFAULT_CREDITSYSTEM");
/*static*/ ACesiumCreditSystem*
ACesiumCreditSystem::GetDefaultCreditSystem(const UObject* WorldContextObject) {
// Blueprint loading can only happen in a constructor, so we instantiate a
// loader object that retrieves the blueprint class in its constructor. We can
// destroy the loader immediately once it's done since it will have already
// set CesiumCreditSystemBP.
if (!CesiumCreditSystemBP) {
UCesiumCreditSystemBPLoader* bpLoader =
NewObject<UCesiumCreditSystemBPLoader>();
CesiumCreditSystemBP = bpLoader->CesiumCreditSystemBP.LoadSynchronous();
bpLoader->ConditionalBeginDestroy();
}
UWorld* world = WorldContextObject->GetWorld();
// This method can be called by actors even when opening the content browser.
if (!IsValid(world)) {
return nullptr;
}
UE_LOG(
LogCesium,
Verbose,
TEXT("World name for GetDefaultCreditSystem: %s"),
*world->GetFullName());
// Note: The actor iterator will be created with the
// "EActorIteratorFlags::SkipPendingKill" flag,
// meaning that we don't have to handle objects
// that have been deleted. (This is the default,
// but made explicit here)
ACesiumCreditSystem* pCreditSystem = nullptr;
EActorIteratorFlags flags = EActorIteratorFlags::OnlyActiveLevels |
EActorIteratorFlags::SkipPendingKill;
for (TActorIterator<AActor> actorIterator(
world,
ACesiumCreditSystem::StaticClass(),
flags);
actorIterator;
++actorIterator) {
AActor* actor = *actorIterator;
if (actor->GetLevel() == world->PersistentLevel &&
actor->ActorHasTag(DEFAULT_CREDITSYSTEM_TAG)) {
pCreditSystem = Cast<ACesiumCreditSystem>(actor);
break;
}
}
if (!pCreditSystem) {
// Legacy method of finding Georeference, for backwards compatibility with
// existing projects
ACesiumCreditSystem* pCreditSystemCandidate =
findValidDefaultCreditSystem(world->PersistentLevel);
// Test if PendingKill
if (IsValid(pCreditSystemCandidate)) {
pCreditSystem = pCreditSystemCandidate;
}
}
if (!pCreditSystem) {
UE_LOG(
LogCesium,
Verbose,
TEXT("Creating default Credit System for actor %s"),
*WorldContextObject->GetName());
// Spawn georeference in the persistent level
FActorSpawnParameters spawnParameters;
spawnParameters.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
spawnParameters.OverrideLevel = world->PersistentLevel;
pCreditSystem = world->SpawnActor<ACesiumCreditSystem>(
Cast<UClass>(CesiumCreditSystemBP),
spawnParameters);
// Null check so the editor doesn't crash when it makes arbitrary calls to
// this function without a valid world context object.
if (pCreditSystem) {
pCreditSystem->Tags.Add(DEFAULT_CREDITSYSTEM_TAG);
}
} else {
UE_LOG(
LogCesium,
Verbose,
TEXT("Using existing CreditSystem %s for actor %s"),
*pCreditSystem->GetName(),
*WorldContextObject->GetName());
}
return pCreditSystem;
}
ACesiumCreditSystem::ACesiumCreditSystem()
: AActor(),
_pCreditSystem(std::make_shared<CesiumUtility::CreditSystem>()),
_lastCreditsCount(0) {
PrimaryActorTick.bCanEverTick = true;
#if WITH_EDITOR
this->SetIsSpatiallyLoaded(false);
#endif
}
void ACesiumCreditSystem::BeginPlay() {
Super::BeginPlay();
if (checkIfInSubLevel(this))
return;
this->updateCreditsViewport(true);
}
void ACesiumCreditSystem::EndPlay(const EEndPlayReason::Type EndPlayReason) {
this->removeCreditsFromViewports();
Super::EndPlay(EndPlayReason);
}
static const FName LevelEditorName("LevelEditor");
void ACesiumCreditSystem::OnConstruction(const FTransform& Transform) {
Super::OnConstruction(Transform);
if (checkIfInSubLevel(this))
return;
this->updateCreditsViewport(false);
#if WITH_EDITOR
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(LevelEditorName);
if (pLevelEditorModule && !GetWorld()->IsGameWorld()) {
pLevelEditorModule->OnRedrawLevelEditingViewports().RemoveAll(this);
pLevelEditorModule->OnRedrawLevelEditingViewports().AddUObject(
this,
&ACesiumCreditSystem::OnRedrawLevelEditingViewports);
FEditorSupportDelegates::CleanseEditor.RemoveAll(this);
FEditorSupportDelegates::CleanseEditor.AddUObject(
this,
&ACesiumCreditSystem::OnCleanseEditor);
FEditorDelegates::PreBeginPIE.RemoveAll(this);
FEditorDelegates::PreBeginPIE.AddUObject(
this,
&ACesiumCreditSystem::OnPreBeginPIE);
FGameDelegates::Get().GetEndPlayMapDelegate().RemoveAll(this);
FGameDelegates::Get().GetEndPlayMapDelegate().AddUObject(
this,
&ACesiumCreditSystem::OnEndPIE);
}
#endif
}
void ACesiumCreditSystem::BeginDestroy() {
#if WITH_EDITOR
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(LevelEditorName);
if (pLevelEditorModule) {
pLevelEditorModule->OnRedrawLevelEditingViewports().RemoveAll(this);
}
FEditorSupportDelegates::CleanseEditor.RemoveAll(this);
FEditorDelegates::PreBeginPIE.RemoveAll(this);
FEditorDelegates::EndPIE.RemoveAll(this);
#endif
Super::BeginDestroy();
}
void ACesiumCreditSystem::updateCreditsViewport(bool recreateWidget) {
if (IsRunningDedicatedServer())
return;
if (!IsValid(GetWorld()))
return;
if (!IsValid(CreditsWidget) || recreateWidget) {
CreditsWidget =
CreateWidget<UScreenCreditsWidget>(GetWorld(), CreditsWidgetClass);
}
#if WITH_EDITOR
FLevelEditorModule* pLevelEditorModule =
FModuleManager::GetModulePtr<FLevelEditorModule>(LevelEditorName);
if (pLevelEditorModule && !GetWorld()->IsGameWorld()) {
// Add credits to the active editor viewport
TSharedPtr<IAssetViewport> pActiveViewport =
pLevelEditorModule->GetFirstActiveViewport();
if (pActiveViewport.IsValid() &&
this->_pLastEditorViewport != pActiveViewport) {
this->removeCreditsFromViewports();
if (!pActiveViewport->HasPlayInEditorViewport()) {
auto pSlateWidget = CreditsWidget->TakeWidget();
pActiveViewport->AddOverlayWidget(pSlateWidget);
this->_pLastEditorViewport = pActiveViewport;
}
}
return;
}
this->removeCreditsFromViewports();
#endif
// Add credits to a game viewport
CreditsWidget->AddToViewport();
}
void ACesiumCreditSystem::removeCreditsFromViewports() {
#if WITH_EDITOR
if (this->_pLastEditorViewport.IsValid()) {
auto pPinned = this->_pLastEditorViewport.Pin();
pPinned->RemoveOverlayWidget(CreditsWidget->TakeWidget());
this->_pLastEditorViewport = nullptr;
}
#endif
if (IsValid(CreditsWidget)) {
CreditsWidget->RemoveFromParent();
}
}
#if WITH_EDITOR
void ACesiumCreditSystem::OnRedrawLevelEditingViewports(bool) {
this->updateCreditsViewport(false);
}
void ACesiumCreditSystem::OnPreBeginPIE(bool bIsSimulating) {
// When we start play-in-editor, remove the editor viewport credits.
// The game will often reuse the same viewport, and we don't want to show
// two sets of credits.
this->removeCreditsFromViewports();
}
void ACesiumCreditSystem::OnEndPIE() { this->updateCreditsViewport(false); }
void ACesiumCreditSystem::OnCleanseEditor() {
// This is called late in the process of unloading a level.
this->removeCreditsFromViewports();
}
#endif
bool ACesiumCreditSystem::ShouldTickIfViewportsOnly() const { return true; }
void ACesiumCreditSystem::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
if (!_pCreditSystem || !IsValid(CreditsWidget)) {
return;
}
const std::vector<CesiumUtility::Credit>& creditsToShowThisFrame =
_pCreditSystem->getCreditsToShowThisFrame();
// if the credit list has changed, we want to reformat the credits
CreditsUpdated =
creditsToShowThisFrame.size() != _lastCreditsCount ||
_pCreditSystem->getCreditsToNoLongerShowThisFrame().size() > 0;
if (CreditsUpdated) {
FString OnScreenCredits;
FString Credits;
_lastCreditsCount = creditsToShowThisFrame.size();
bool firstCreditOnScreen = true;
for (int i = 0; i < creditsToShowThisFrame.size(); i++) {
const CesiumUtility::Credit& credit = creditsToShowThisFrame[i];
FString CreditRtf;
const std::string& html = _pCreditSystem->getHtml(credit);
auto htmlFind = _htmlToRtf.find(html);
if (htmlFind != _htmlToRtf.end()) {
CreditRtf = htmlFind->second;
} else {
CreditRtf = ConvertHtmlToRtf(html);
_htmlToRtf.insert({html, CreditRtf});
}
if (_pCreditSystem->shouldBeShownOnScreen(credit)) {
if (firstCreditOnScreen) {
firstCreditOnScreen = false;
} else {
OnScreenCredits += TEXT(" \u2022 ");
}
OnScreenCredits += CreditRtf;
} else {
if (i != 0) {
Credits += "\n";
}
Credits += CreditRtf;
}
}
if (!Credits.IsEmpty()) {
OnScreenCredits += "<credits url=\"popup\" text=\" Data attribution\"/>";
}
CreditsWidget->SetCredits(Credits, OnScreenCredits);
}
_pCreditSystem->startNextFrame();
}
namespace {
void convertHtmlToRtf(
std::string& output,
std::string& parentUrl,
TidyDoc tdoc,
TidyNode tnod,
UScreenCreditsWidget* CreditsWidget) {
TidyNode child;
TidyBuffer buf;
tidyBufInit(&buf);
for (child = tidyGetChild(tnod); child; child = tidyGetNext(child)) {
if (tidyNodeIsText(child)) {
tidyNodeGetText(tdoc, child, &buf);
if (buf.bp) {
std::string text = reinterpret_cast<const char*>(buf.bp);
tidyBufClear(&buf);
// could not find correct option in tidy html to not add new lines
if (text.size() != 0 && text[text.size() - 1] == '\n') {
text.pop_back();
}
if (!parentUrl.empty()) {
output +=
"<credits url=\"" + parentUrl + "\"" + " text=\"" + text + "\"/>";
} else {
output += text;
}
}
} else if (tidyNodeGetId(child) == TidyTagId::TidyTag_IMG) {
auto srcAttr = tidyAttrGetById(child, TidyAttrId::TidyAttr_SRC);
if (srcAttr) {
auto srcValue = tidyAttrValue(srcAttr);
if (srcValue) {
output += "<credits id=\"" +
CreditsWidget->LoadImage(
std::string(reinterpret_cast<const char*>(srcValue))) +
"\"";
if (!parentUrl.empty()) {
output += " url=\"" + parentUrl + "\"";
}
output += "/>";
}
}
}
auto hrefAttr = tidyAttrGetById(child, TidyAttrId::TidyAttr_HREF);
if (hrefAttr) {
auto hrefValue = tidyAttrValue(hrefAttr);
parentUrl = std::string(reinterpret_cast<const char*>(hrefValue));
}
convertHtmlToRtf(output, parentUrl, tdoc, child, CreditsWidget);
}
tidyBufFree(&buf);
}
} // namespace
FString ACesiumCreditSystem::ConvertHtmlToRtf(std::string html) {
TidyDoc tdoc;
TidyBuffer tidy_errbuf = {0};
int err;
tdoc = tidyCreate();
tidyOptSetBool(tdoc, TidyForceOutput, yes);
tidyOptSetInt(tdoc, TidyWrapLen, 0);
tidyOptSetInt(tdoc, TidyNewline, TidyLF);
tidySetErrorBuffer(tdoc, &tidy_errbuf);
html = "<!DOCTYPE html><html><body>" + html + "</body></html>";
std::string output, url;
err = tidyParseString(tdoc, html.c_str());
if (err < 2) {
convertHtmlToRtf(output, url, tdoc, tidyGetRoot(tdoc), CreditsWidget);
}
tidyBufFree(&tidy_errbuf);
tidyRelease(tdoc);
return UTF8_TO_TCHAR(output.c_str());
}