#pragma once #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "WorldMapSubsystem.generated.h" class UWorldMapDefinition; UENUM(BlueprintType) enum class EWorldMapMarkerShape : uint8 { /** A filled disc. Anything that is just somewhere. */ Dot, /** An unfilled ring, so it does not hide what is under it. Objectives, regions of interest. */ Ring, /** A cross. Reads at one pixel where a dot does not, which is what a waypoint needs when zoomed out. */ Cross, /** A triangle pointing along Heading. The body you are in, and anything else with a facing. */ Arrow }; /** * Something to draw on the map at a place in the world. Purely a UI concern - nothing here is replicated and * nothing gameplay does depends on a marker existing. */ USTRUCT(BlueprintType) struct SALTY_API FWorldMapMarker { GENERATED_BODY() /** Centimetres, world space. Z is ignored; the map is a plan. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") FVector WorldLocation = FVector::ZeroVector; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") FText Label; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") FLinearColor Colour = FLinearColor::White; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") EWorldMapMarkerShape Shape = EWorldMapMarkerShape::Dot; /** World yaw in degrees for Arrow: 0 points along world +X, which is to the right on the map. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") float HeadingDegrees = 0.f; /** Drawn size in pixels, so a marker stays the same size however far the map is zoomed out. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map") float SizePx = 7.f; }; /** * The map's registry: which definition this level has a map from, and what is on it. * * A world subsystem because a map belongs to a map. Markers are registered by whoever owns the thing being * marked - an actor in BeginPlay, unregistering in EndPlay - rather than found by the map, because a widget * that ran GetAllActorsOfClass to draw itself would be exactly the world search the conventions forbid. * * The local player's own body is deliberately NOT a registered marker: it moves every frame, and a registry * entry rewritten every frame is a registry being used as a variable. The widget asks for it instead. */ UCLASS() class SALTY_API UWorldMapSubsystem : public UWorldSubsystem { GENERATED_BODY() public: virtual bool ShouldCreateSubsystem(UObject* Outer) const override; virtual void Deinitialize() override; /** * The definition whose Level is this world, loading it the first time it is asked for. Null when this level * has no map, which is the normal answer in L_Gym and is not an error. */ UFUNCTION(BlueprintCallable, Category = "World Map") const UWorldMapDefinition* GetDefinition() const; /** Adds a marker and returns its handle. Keep the handle; it is the only way to move or remove it. */ UFUNCTION(BlueprintCallable, Category = "World Map") FGuid AddMarker(const FWorldMapMarker& Marker); /** Replaces a marker wholesale. False when the handle is not one of ours. */ UFUNCTION(BlueprintCallable, Category = "World Map") bool UpdateMarker(const FGuid& Handle, const FWorldMapMarker& Marker); UFUNCTION(BlueprintCallable, Category = "World Map") bool RemoveMarker(const FGuid& Handle); const TMap& GetMarkers() const { return Markers; } /** * Where the local player is, as a marker, or false when there is no local body to speak of - a dedicated * server, or a client before possession. The heading is the control rotation rather than the body's, because * on a map a player expects the arrow to point where they are looking. */ UFUNCTION(BlueprintCallable, Category = "World Map") bool GetLocalPlayerMarker(FWorldMapMarker& OutMarker) const; /** * Emits world_map_opened. Every way of opening the map calls this - the console command today, a HUD key * when there is one - so the count is of maps opened and not of the ways of opening them. */ UFUNCTION(BlueprintCallable, Category = "World Map") void NoteMapOpened(FName LayerId, float MetresPerPixel, const FString& OpenedBy); /** Raised when a marker is added, moved or removed. The widget subscribes; it never polls. */ DECLARE_MULTICAST_DELEGATE(FOnWorldMapMarkersChanged); FOnWorldMapMarkersChanged OnMarkersChanged; private: /** Every WorldMap definition whose Level is this world. Searched once; the answer, including none, sticks. */ void ResolveDefinition() const; UPROPERTY(Transient) mutable TObjectPtr Definition; mutable bool bResolved = false; TMap Markers; };