// Console commands of the Tender (DESIGN_UE.md section 9.3, M3): Board, Launch, Dock, Exit, Enter, Teleport, LookAt, Set. // They call the same controller actions the keys call, so a scenario goes through the player's path. #include "Debug/DescentCommandUtil.h" #include "Player/DescentPlayerController.h" #include "Vehicles/CarrierCarousel.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "World/DescentTerrainSubsystem.h" #include "World/DescentWorldDef.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Engine/World.h" #include "HAL/IConsoleManager.h" namespace { ADescentPlayerController* TenderCmdPC(UWorld* World, const TCHAR* Cmd) { ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; if (!PC || !PC->GetCarousel() || !PC->GetDescentCharacter()) { Descent::Cmd::Fail(World, Cmd, TEXT("needs a running game with the character possessed and the carrier's carousel")); return nullptr; } return PC; } /** Tender number Arg (0..2) of the fleet, or null. */ AKazahTender* TenderCmdByIndex(UWorld* World, const FString& Arg) { const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World); return (Fleet && Arg.IsNumeric()) ? Fleet->At(FCString::Atoi(*Arg)) : nullptr; } /** The Tender a pose command acts on: the one the player sits in, else Tender 0. */ AKazahTender* TenderCmdTarget(UWorld* World) { const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; if (PC && PC->GetTender()) { return PC->GetTender(); } const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World); return Fleet ? Fleet->At(0) : nullptr; } void CmdTenderBoard(const TArray& Args, UWorld* World) { ADescentPlayerController* PC = TenderCmdPC(World, TEXT("Descent.Tender.Board")); if (!PC) { return; } AKazahTender* T = Args.Num() > 0 ? TenderCmdByIndex(World, Args[0]) : PC->GetCarousel()->TenderAtBoard(); FString Why; if (!PC->BeginBoard(T, &Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Board"), FString::Printf(TEXT("refused: %s (usage: Descent.Tender.Board [index]; the Tender must hang at BOARD and the player be on foot)"), *Why)); } } void CmdTenderLaunch(const TArray&, UWorld* World) { ADescentPlayerController* PC = TenderCmdPC(World, TEXT("Descent.Tender.Launch")); FString Why; if (PC && !PC->Launch(&Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Launch"), FString::Printf(TEXT("refused: %s"), *Why)); } } void CmdTenderDock(const TArray&, UWorld* World) { ADescentPlayerController* PC = TenderCmdPC(World, TEXT("Descent.Tender.Dock")); FString Why; if (PC && !PC->Dock(&Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Dock"), FString::Printf(TEXT("refused: %s"), *Why)); } } void CmdTenderExit(const TArray&, UWorld* World) { ADescentPlayerController* PC = TenderCmdPC(World, TEXT("Descent.Tender.Exit")); if (!PC) { return; } FString Why; const bool bOk = PC->GetMode() == EDescentMode::Docked ? PC->LeaveDocked(&Why) : PC->ExitToSwim(&Why); if (!bOk) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Exit"), FString::Printf(TEXT("refused: %s"), *Why)); } } void CmdTenderEnter(const TArray& Args, UWorld* World) { ADescentPlayerController* PC = TenderCmdPC(World, TEXT("Descent.Tender.Enter")); if (!PC) { return; } FString Why; // With an index the range is not checked (a test tool); without one it is the player's E: the nearest free Tender within 4.5 m. AKazahTender* T = Args.Num() > 0 ? TenderCmdByIndex(World, Args[0]) : nullptr; if (!PC->EnterTender(T, Args.Num() > 0, &Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Enter"), FString::Printf(TEXT("refused: %s"), *Why)); } } void CmdTenderTeleport(const TArray& Args, UWorld* World) { AKazahTender* T = TenderCmdTarget(World); TArray N; FVector Pos, Look; bool bLook = false; if (T && Args.Num() == 1) { // a named place of the world (the Tender form of Descent.World.Goto) const Descent::World::FPlace* Place = Descent::World::FDescentWorldDef::Get().FindPlace(FName(*Args[0])); if (!Place) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Teleport"), FString::Printf(TEXT("no place called '%s'"), *Args[0])); return; } Pos = Place->PosM; Look = Place->LookAtM; bLook = true; } else if (T && Descent::Cmd::ParseNumbers(Args, 0, 3, N)) { Pos = FVector(N[0], N[1], N[2]); } else { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Teleport"), TEXT("usage: Descent.Tender.Teleport x y z | (world metres; moves the player's Tender)")); return; } if (!T->Flight->IsOut()) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Teleport"), TEXT("the Tender is in its cradle: launch it first")); return; } const FRotator Rot = bLook ? (Look - Pos).Rotation() : T->GetActorRotation(); T->SetActorLocationAndRotation(Descent::MetresToUU(Pos), FRotator(Rot.Pitch, Rot.Yaw, 0.0), false, nullptr, ETeleportType::TeleportPhysics); T->Flight->VelMps = FVector::ZeroVector; T->Flight->SyncFromActor(); if (UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(World)) { Terrain->RequestReplan(); } UE_LOG(LogDescent, Display, TEXT("Tender.Teleport: %s at %.1f %.1f %.1f m"), T->Flight->Spec().Id, Pos.X, Pos.Y, Pos.Z); } void CmdTenderLookAt(const TArray& Args, UWorld* World) { AKazahTender* T = TenderCmdTarget(World); TArray N; if (!T || !T->Flight->IsOut() || !Descent::Cmd::ParseNumbers(Args, 0, 3, N)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.LookAt"), TEXT("usage: Descent.Tender.LookAt x y z (world metres; the player's Tender must be out)")); return; } const FRotator Rot = (FVector(N[0], N[1], N[2]) - Descent::UUToMetres(T->GetActorLocation())).Rotation(); T->SetActorRotation(FRotator(Rot.Pitch, Rot.Yaw, 0.0), ETeleportType::TeleportPhysics); T->Flight->SyncFromActor(); } void CmdTenderSet(const TArray& Args, UWorld* World) { TArray N; AKazahTender* T = Args.Num() > 2 ? TenderCmdByIndex(World, Args[2]) : TenderCmdTarget(World); if (!T || Args.Num() < 2 || !Descent::Cmd::ParseNumbers(Args, 1, 1, N)) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Set"), TEXT("usage: Descent.Tender.Set value [index] (hull in HP, power in percent)")); return; } if (Args[0].Equals(TEXT("hull"), ESearchCase::IgnoreCase)) { T->Flight->HullHP = FMath::Clamp(N[0], 0.0, T->Flight->Stats().HullMax); if (N[0] <= 0.0) { T->Flight->bImploded = true; } } else if (Args[0].Equals(TEXT("power"), ESearchCase::IgnoreCase)) { T->Flight->PowerPct = FMath::Clamp(N[0], 0.0, 100.0); } else { Descent::Cmd::Fail(World, TEXT("Descent.Tender.Set"), TEXT("unknown stat (hull, power)")); } } void CmdTenderGiveOre(const TArray& Args, UWorld* World) { // Section 9.3: puts ore chunks into the hold, in arrival order, up to the hold's capacity. The hold is the real cargo state: // the cargo display, the dash line and the `tenders` section read it. TArray N; AKazahTender* T = Args.Num() > 2 ? TenderCmdByIndex(World, Args[2]) : TenderCmdTarget(World); const FString Type = Args.Num() > 0 ? Args[0].ToLower() : FString(); if (!T || !Descent::Cmd::ParseNumbers(Args, 1, 1, N) || !(Type == TEXT("ferrite") || Type == TEXT("cobalt") || Type == TEXT("pearl"))) { Descent::Cmd::Fail(World, TEXT("Descent.Tender.GiveOre"), TEXT("usage: Descent.Tender.GiveOre n [index]")); return; } const int32 Cap = T->Flight->Stats().HoldCap; int32 Added = 0; for (int32 I = 0; I < FMath::RoundToInt32(N[0]) && T->Flight->HoldOrder.Num() < Cap; ++I, ++Added) { T->Flight->AddToHold(FName(*Type)); } UE_LOG(LogDescent, Display, TEXT("Tender.GiveOre: %d %s into %s, hold %d/%d"), Added, *Type, T->Flight->Spec().Id, T->Flight->HoldOrder.Num(), Cap); } void CmdTenderDrillSuck(const TArray& Args, UWorld* World, bool bDrill) { AKazahTender* T = TenderCmdTarget(World); bool bValid = false; const bool bOn = Args.Num() > 0 ? Descent::Cmd::ParseBool(Args[0], bValid) : false; if (!T || !bValid) { Descent::Cmd::Fail(World, bDrill ? TEXT("Descent.Tender.Drill") : TEXT("Descent.Tender.Suck"), TEXT("usage: <0 or 1> (holds LMB / RMB of the Tender; 0 gives it back to the intent)")); return; } (bDrill ? T->Flight->DebugDrill : T->Flight->DebugSuck) = bOn ? 1 : -1; } void CmdTenderDrill(const TArray& Args, UWorld* World) { CmdTenderDrillSuck(Args, World, true); } void CmdTenderSuck(const TArray& Args, UWorld* World) { CmdTenderDrillSuck(Args, World, false); } void CmdUpgrade(const TArray& Args, UWorld* World) { // Section 9.3: sets a field of the upgrade state (every Tender of the fleet shares it). TArray N; const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World); if (!Fleet || Args.Num() < 2 || !Descent::Cmd::ParseNumbers(Args, 1, 1, N)) { Descent::Cmd::Fail(World, TEXT("Descent.Upgrade"), TEXT("usage: Descent.Upgrade ")); return; } const int32 L = FMath::RoundToInt32(N[0]); const FString K = Args[0].ToLower(); for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* T = W.Get(); if (!T) { continue; } Descent::Tender::FTenderUpgrades& U = T->Flight->Upgrades; const double HullFrac = T->Flight->HullHP / T->Flight->Stats().HullMax; if (K == TEXT("hullmk")) { U.HullMk = L; T->Flight->HullHP = HullFrac * T->Flight->Stats().HullMax; } else if (K == TEXT("depthmk")) { U.DepthMk = L; } else if (K == TEXT("thrustmk")) { U.ThrustMk = L; } else if (K == TEXT("holdmk")) { U.HoldMk = L; } else if (K == TEXT("drillmk")) { U.DrillMk = L; } else if (K == TEXT("suctionmk")) { U.SuctionMk = L; } else if (K == TEXT("cellmk")) { U.bPowerCell2 = L > 0; } else if (K == TEXT("sonar")) { U.bSonarModule = L > 0; } else if (K == TEXT("laser")) { U.bLaser = L > 0; if (!U.bLaser) { T->Flight->bLaserHead = false; } } else { Descent::Cmd::Fail(World, TEXT("Descent.Upgrade"), TEXT("unknown upgrade")); return; } } } FAutoConsoleCommandWithWorldAndArgs GTenderCmdDrill(TEXT("Descent.Tender.Drill"), TEXT("Descent.Tender.Drill <0 or 1> - hold the drill (LMB)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderDrill)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdSuck(TEXT("Descent.Tender.Suck"), TEXT("Descent.Tender.Suck <0 or 1> - hold the suction (RMB)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderSuck)); FAutoConsoleCommandWithWorldAndArgs GCmdUpgrade(TEXT("Descent.Upgrade"), TEXT("Descent.Upgrade - set an upgrade level"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdUpgrade)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdGiveOre(TEXT("Descent.Tender.GiveOre"), TEXT("Descent.Tender.GiveOre n [i] - put ore chunks into the hold"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderGiveOre)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdBoard(TEXT("Descent.Tender.Board"), TEXT("Descent.Tender.Board [i] - board the Tender that hangs at BOARD (Foot -> Board -> Docked)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderBoard)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdLaunch(TEXT("Descent.Tender.Launch"), TEXT("Descent.Tender.Launch - Space in the cradle (Docked -> Launch -> Sub)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderLaunch)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdDock(TEXT("Descent.Tender.Dock"), TEXT("Descent.Tender.Dock - E under the moon pool (Sub -> Dock -> Docked)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderDock)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdExit(TEXT("Descent.Tender.Exit"), TEXT("Descent.Tender.Exit - leave the Tender: to the pool rim when docked, to swim when out"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderExit)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdEnter(TEXT("Descent.Tender.Enter"), TEXT("Descent.Tender.Enter [i] - a swimmer enters a Tender that is out (no index: within 4.5 m)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderEnter)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdTeleport(TEXT("Descent.Tender.Teleport"), TEXT("Descent.Tender.Teleport x y z | - move the player's Tender (world metres)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderTeleport)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdLookAt(TEXT("Descent.Tender.LookAt"), TEXT("Descent.Tender.LookAt x y z - point the player's Tender at a world point"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderLookAt)); FAutoConsoleCommandWithWorldAndArgs GTenderCmdSet(TEXT("Descent.Tender.Set"), TEXT("Descent.Tender.Set v [i]"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdTenderSet)); }