Files
UnrealPrototyping/Source/Salty/World/WorldMapSubsystem.cpp
T
2026-09-25 17:02:24 +03:00

171 lines
4.9 KiB
C++

#include "World/WorldMapSubsystem.h"
#include "Core/TelemetrySubsystem.h"
#include "Engine/AssetManager.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "Salty.h"
#include "Telemetry/TelemetryEvent.h"
#include "Telemetry/TelemetryEvents.h"
#include "World/WorldMapDefinition.h"
bool UWorldMapSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
if (!Super::ShouldCreateSubsystem(Outer))
{
return false;
}
// Game and PIE worlds only. An editor preview world or a thumbnail scene has no map and no one to show it to.
const UWorld* World = Cast<UWorld>(Outer);
return World && (World->IsGameWorld());
}
void UWorldMapSubsystem::Deinitialize()
{
Markers.Reset();
OnMarkersChanged.Clear();
Definition = nullptr;
bResolved = false;
Super::Deinitialize();
}
void UWorldMapSubsystem::ResolveDefinition() const
{
bResolved = true;
Definition = nullptr;
const UWorld* World = GetWorld();
if (!World)
{
return;
}
// PIE renames the level package (/Game/Maps/UEDPIE_0_L_World), so the definition's own path would never
// match what the world says it is. Strip it, or the map works in a cooked build and not in the editor.
const FString LevelPackage = UWorld::RemovePIEPrefix(World->GetOutermost()->GetName());
// IsInitialized, not IsValid: the manager is always constructed now and IsValid is deprecated for saying
// so. The guard stays because this can be reached before UEngine::InitializeObjectReferences has run.
if (!UAssetManager::IsInitialized())
{
return;
}
UAssetManager& Manager = UAssetManager::Get();
TArray<FPrimaryAssetId> Ids;
Manager.GetPrimaryAssetIdList(UWorldMapDefinition::AssetType, Ids);
if (Ids.Num() == 0)
{
UE_LOG(LogSalty, Verbose,
TEXT("World map: no %s assets are registered. Add a PrimaryAssetTypesToScan entry in DefaultGame.ini "
"and run Scripts/Authoring/build_world_map.sh."),
*UWorldMapDefinition::AssetType.ToString());
return;
}
for (const FPrimaryAssetId& Id : Ids)
{
const FSoftObjectPath Path = Manager.GetPrimaryAssetPath(Id);
// Synchronous, because this happens once when a map is first opened and an asynchronous load would
// mean a frame of blank map for no gain.
const UWorldMapDefinition* Candidate = Cast<UWorldMapDefinition>(Path.TryLoad());
if (Candidate && Candidate->Level.GetLongPackageName() == LevelPackage)
{
Definition = Candidate;
UE_LOG(LogSalty, Log, TEXT("World map: %s covers %s - %s"),
*Id.ToString(), *LevelPackage, *Candidate->Projection.ToString());
return;
}
}
// Deliberately not "fall back to the only one there is". L_Gym showing L_World's map would be a map that
// lies about where you are, which is worse than no map.
UE_LOG(LogSalty, Verbose, TEXT("World map: none of the %d definitions is a map of %s."), Ids.Num(), *LevelPackage);
}
const UWorldMapDefinition* UWorldMapSubsystem::GetDefinition() const
{
if (!bResolved)
{
ResolveDefinition();
}
return Definition;
}
FGuid UWorldMapSubsystem::AddMarker(const FWorldMapMarker& Marker)
{
const FGuid Handle = FGuid::NewGuid();
Markers.Add(Handle, Marker);
OnMarkersChanged.Broadcast();
return Handle;
}
bool UWorldMapSubsystem::UpdateMarker(const FGuid& Handle, const FWorldMapMarker& Marker)
{
if (FWorldMapMarker* Existing = Markers.Find(Handle))
{
*Existing = Marker;
OnMarkersChanged.Broadcast();
return true;
}
return false;
}
bool UWorldMapSubsystem::RemoveMarker(const FGuid& Handle)
{
if (Markers.Remove(Handle) > 0)
{
OnMarkersChanged.Broadcast();
return true;
}
return false;
}
void UWorldMapSubsystem::NoteMapOpened(FName LayerId, float MetresPerPixel, const FString& OpenedBy)
{
const UWorld* World = GetWorld();
const UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
UTelemetrySubsystem* Telemetry = GameInstance ? GameInstance->GetSubsystem<UTelemetrySubsystem>() : nullptr;
if (!Telemetry)
{
return;
}
Telemetry->Emit(TelemetryEvents::WorldMapOpened, FTelemetryPayload()
.Set(TEXT("layer"), LayerId)
.Set(TEXT("metres_per_pixel"), MetresPerPixel)
.Set(TEXT("opened_by"), OpenedBy));
}
bool UWorldMapSubsystem::GetLocalPlayerMarker(FWorldMapMarker& OutMarker) const
{
const UWorld* World = GetWorld();
const APlayerController* Controller = World ? World->GetFirstPlayerController() : nullptr;
if (!Controller)
{
return false;
}
// The spectator counts: someone flying the level with no pawn is still somewhere, and that is exactly when
// a map is most useful.
const AActor* Body = Controller->GetPawnOrSpectator();
if (!Body)
{
Body = Controller->GetViewTarget();
}
if (!Body)
{
return false;
}
OutMarker = FWorldMapMarker();
OutMarker.WorldLocation = Body->GetActorLocation();
OutMarker.Shape = EWorldMapMarkerShape::Arrow;
OutMarker.Colour = FLinearColor(1.f, 0.95f, 0.35f);
OutMarker.SizePx = 13.f;
OutMarker.HeadingDegrees = static_cast<float>(Controller->GetControlRotation().Yaw);
return true;
}