202 lines
7.0 KiB
C++
202 lines
7.0 KiB
C++
// The bs.* console commands. Every one marks the session as cheated through the telemetry subsystem, so a
|
|
// playtest file that had a cheat in it can be filtered out later. Each step adds its own commands here.
|
|
|
|
#include "Core/TelemetrySubsystem.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/GameViewportClient.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "Salty.h"
|
|
#include "Styling/CoreStyle.h"
|
|
#include "UI/SWorldMap.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Widgets/Layout/SBox.h"
|
|
#include "Widgets/SBoxPanel.h"
|
|
#include "World/WorldMapDefinition.h"
|
|
#include "World/WorldMapSubsystem.h"
|
|
|
|
namespace
|
|
{
|
|
UTelemetrySubsystem* TelemetryFor(UWorld* World)
|
|
{
|
|
const UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
|
|
return GameInstance ? GameInstance->GetSubsystem<UTelemetrySubsystem>() : nullptr;
|
|
}
|
|
|
|
// The world map overlay, per world. Per world rather than one static, because PIE with two clients is two
|
|
// worlds in one process and a single pointer would hand the second client the first one's widget.
|
|
struct FWorldMapOverlay
|
|
{
|
|
TWeakObjectPtr<UWorld> World;
|
|
TSharedPtr<SWorldMap> Map;
|
|
TSharedPtr<SWidget> Container;
|
|
};
|
|
TArray<FWorldMapOverlay> WorldMapOverlays;
|
|
|
|
FWorldMapOverlay* FindOverlay(const UWorld* World)
|
|
{
|
|
return WorldMapOverlays.FindByPredicate([World](const FWorldMapOverlay& Entry) { return Entry.World.Get() == World; });
|
|
}
|
|
|
|
void CloseWorldMap(UWorld* World)
|
|
{
|
|
const int32 Index = WorldMapOverlays.IndexOfByPredicate(
|
|
[World](const FWorldMapOverlay& Entry) { return Entry.World.Get() == World; });
|
|
if (Index == INDEX_NONE)
|
|
{
|
|
return;
|
|
}
|
|
if (UGameViewportClient* Viewport = World ? World->GetGameViewport() : nullptr)
|
|
{
|
|
Viewport->RemoveViewportWidgetContent(WorldMapOverlays[Index].Container.ToSharedRef());
|
|
}
|
|
if (APlayerController* Controller = World ? World->GetFirstPlayerController() : nullptr)
|
|
{
|
|
Controller->SetInputMode(FInputModeGameOnly());
|
|
Controller->bShowMouseCursor = false;
|
|
}
|
|
WorldMapOverlays.RemoveAt(Index);
|
|
}
|
|
|
|
void OpenWorldMap(UWorld* World)
|
|
{
|
|
UGameViewportClient* Viewport = World ? World->GetGameViewport() : nullptr;
|
|
UWorldMapSubsystem* Subsystem = World ? World->GetSubsystem<UWorldMapSubsystem>() : nullptr;
|
|
if (!Viewport || !Subsystem)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// const_cast for the same reason UWorldMapWidget does it: the subsystem hands out a const view of
|
|
// shared content and the widget holds a strong pointer it cannot make const. Nothing writes to it.
|
|
UWorldMapDefinition* Definition = const_cast<UWorldMapDefinition*>(Subsystem->GetDefinition());
|
|
|
|
FWorldMapOverlay Entry;
|
|
Entry.World = World;
|
|
|
|
// Slate straight into the viewport rather than a UMG asset, so opening the map needs no content at
|
|
// all. When there is a HUD this becomes a key on it and WBP_WorldMap owns the frame instead.
|
|
Entry.Container = SNew(SBox)
|
|
.Padding(48.f)
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FCoreStyle::Get().GetBrush("GenericWhiteBox"))
|
|
.BorderBackgroundColor(FLinearColor(0.f, 0.f, 0.f, 0.85f))
|
|
.Padding(2.f)
|
|
[
|
|
SAssignNew(Entry.Map, SWorldMap)
|
|
.Definition(Definition)
|
|
.MarkerSource(Subsystem)
|
|
]
|
|
];
|
|
|
|
Entry.Map->SetFollowLocalPlayer(true);
|
|
Viewport->AddViewportWidgetContent(Entry.Container.ToSharedRef(), 100);
|
|
|
|
if (APlayerController* Controller = World->GetFirstPlayerController())
|
|
{
|
|
// GameAndUI, not UIOnly: this is a map you glance at, and the game does not stop while it is open.
|
|
FInputModeGameAndUI InputMode;
|
|
InputMode.SetWidgetToFocus(Entry.Container);
|
|
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
|
Controller->SetInputMode(InputMode);
|
|
Controller->bShowMouseCursor = true;
|
|
}
|
|
|
|
Subsystem->NoteMapOpened(Entry.Map->GetLayer(),
|
|
static_cast<float>(Entry.Map->GetMetresPerPixel()), TEXT("bs.WorldMap"));
|
|
|
|
WorldMapOverlays.Add(MoveTemp(Entry));
|
|
}
|
|
|
|
// bs.TelemetryTest: proves the seam end to end from the console. Emits cheat_used and taints the session.
|
|
FAutoConsoleCommandWithWorld CmdTelemetryTest(
|
|
TEXT("bs.TelemetryTest"),
|
|
TEXT("Emits a cheat_used telemetry event and marks the session as cheated."),
|
|
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
|
|
{
|
|
if (UTelemetrySubsystem* Telemetry = TelemetryFor(World))
|
|
{
|
|
Telemetry->MarkCheatUsed(TEXT("bs.TelemetryTest"));
|
|
}
|
|
}));
|
|
|
|
// bs.WorldMap: shows the world map over the viewport, or takes it away again. The only way in until there
|
|
// is a HUD with a key on it; the widget it opens is the one the HUD will open.
|
|
FAutoConsoleCommandWithWorld CmdWorldMap(
|
|
TEXT("bs.WorldMap"),
|
|
TEXT("Toggles the world map. Drag to pan, wheel to zoom, right-double-click to fit."),
|
|
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
|
|
{
|
|
if (!World)
|
|
{
|
|
return;
|
|
}
|
|
if (UTelemetrySubsystem* Telemetry = TelemetryFor(World))
|
|
{
|
|
Telemetry->MarkCheatUsed(TEXT("bs.WorldMap"));
|
|
}
|
|
if (FindOverlay(World))
|
|
{
|
|
CloseWorldMap(World);
|
|
}
|
|
else
|
|
{
|
|
OpenWorldMap(World);
|
|
}
|
|
}));
|
|
|
|
// bs.WorldMapFollow: put the view back on the player and keep it there. Panning drops the follow on purpose
|
|
// - a map that snaps back as you let go cannot be read - so this is the way back to it. Left-double-click
|
|
// on the map does the same thing without a console.
|
|
FAutoConsoleCommandWithWorld CmdWorldMapFollow(
|
|
TEXT("bs.WorldMapFollow"),
|
|
TEXT("Centres the open world map on the local player and follows them again."),
|
|
FConsoleCommandWithWorldDelegate::CreateLambda([](UWorld* World)
|
|
{
|
|
if (UTelemetrySubsystem* Telemetry = TelemetryFor(World))
|
|
{
|
|
Telemetry->MarkCheatUsed(TEXT("bs.WorldMapFollow"));
|
|
}
|
|
FWorldMapOverlay* Overlay = FindOverlay(World);
|
|
if (!Overlay || !Overlay->Map.IsValid())
|
|
{
|
|
UE_LOG(LogSalty, Display, TEXT("bs.WorldMapFollow: the map is not open. bs.WorldMap first."));
|
|
return;
|
|
}
|
|
if (!Overlay->Map->FocusOnLocalPlayer())
|
|
{
|
|
UE_LOG(LogSalty, Display, TEXT("bs.WorldMapFollow: there is no local body to follow."));
|
|
}
|
|
}));
|
|
|
|
// bs.WorldMapLayer <id>: relief, colour, satellite, climate - whatever RawContent/World/MapArt/layers.json
|
|
// says. With no argument it cycles, which is what you want while looking at one place.
|
|
FAutoConsoleCommandWithWorldAndArgs CmdWorldMapLayer(
|
|
TEXT("bs.WorldMapLayer"),
|
|
TEXT("Switches the open world map to a layer by id, or cycles when given none."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
|
|
{
|
|
if (UTelemetrySubsystem* Telemetry = TelemetryFor(World))
|
|
{
|
|
Telemetry->MarkCheatUsed(TEXT("bs.WorldMapLayer"));
|
|
}
|
|
FWorldMapOverlay* Overlay = FindOverlay(World);
|
|
if (!Overlay || !Overlay->Map.IsValid())
|
|
{
|
|
UE_LOG(LogSalty, Display, TEXT("bs.WorldMapLayer: the map is not open. bs.WorldMap first."));
|
|
return;
|
|
}
|
|
if (Args.Num() > 0)
|
|
{
|
|
Overlay->Map->SetLayer(FName(*Args[0]));
|
|
}
|
|
else
|
|
{
|
|
Overlay->Map->NextLayer();
|
|
}
|
|
UE_LOG(LogSalty, Display, TEXT("World map layer: %s"), *Overlay->Map->GetLayer().ToString());
|
|
}));
|
|
}
|