#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(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 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(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() : 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(Controller->GetControlRotation().Yaw); return true; }