#include "Systems/DescentCargo.h" #include "Core/DescentBoot.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentAutomation.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/CarrierBlockout.h" #include "Vehicles/CarrierCarousel.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Camera/CameraComponent.h" #include "Components/InstancedStaticMeshComponent.h" #include "Components/StaticMeshComponent.h" #include "Components/TextRenderComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "Materials/MaterialInstanceDynamic.h" namespace CTune = Descent::Tune::Cargo; namespace { const FLinearColor CargoOreColour[3] = { FLinearColor(1.0f, 0.33f, 0.05f), FLinearColor(0.05f, 0.78f, 1.0f), FLinearColor(1.0f, 0.1f, 0.68f) }; UStaticMesh* CargoCube() { return LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); } UMaterialInstanceDynamic* CargoSurface(UObject* Outer, const FLinearColor& Base, const FLinearColor& Emissive, float Strength) { UMaterialInterface* Parent = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentSurface.M_DescentSurface"), nullptr, LOAD_NoWarn); if (!Parent) { return nullptr; } UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Parent, Outer); MID->SetVectorParameterValue(TEXT("BaseColor"), Base); MID->SetVectorParameterValue(TEXT("EmissiveColor"), Emissive); MID->SetScalarParameterValue(TEXT("EmissiveStrength"), Strength); MID->SetScalarParameterValue(TEXT("Roughness"), 0.6f); return MID; } void CargoToast(UWorld* World, const FString& Text, EDescentHudToastKind Kind = EDescentHudToastKind::Info) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World)) { Hud->Model().PushToast(Text, Kind); } } } // ---- pure parts ------------------------------------------------------------------------------------------------------ namespace Descent::Cargo { FName OreName(uint8 Type) { static const FName N[3] = { TEXT("ferrite"), TEXT("cobalt"), TEXT("pearl") }; return N[FMath::Min(Type, 2)]; } FName RefinedName(uint8 Type) { static const FName N[3] = { TEXT("ingot"), TEXT("wafer"), TEXT("lattice") }; return N[FMath::Min(Type, 2)]; } int32 OreTypeOf(FName Ore) { for (int32 T = 0; T < 3; ++T) { if (OreName(static_cast(T)) == Ore) { return T; } } return -1; } int32 RefinedTypeOf(FName Item) { for (int32 T = 0; T < 3; ++T) { if (RefinedName(static_cast(T)) == Item) { return T; } } return -1; } const TArray& OrePipeRouteM() { // Section 6.3, carrier-local: belly coupling, riser, the unload boom along the starboard pool edge, up the bay wall, under the bay // ceiling, above the D2 gallery, through the bay wall along the MID DECK corridor ceiling, into the CARGO BAY, down into the packer. static const TArray Route = { FVector(-29.37, 6.00, -7.10), FVector(-29.37, 6.00, -7.35), FVector(-22.80, 8.75, -7.35), FVector(-22.80, 8.75, 0.90), FVector(-19.20, 8.75, 0.90), FVector(-19.20, 0.60, 0.90), FVector(18.60, 0.60, 0.90), FVector(18.60, -5.00, 0.90), FVector(18.60, -5.00, -0.60) }; return Route; } double RouteLengthM(const TArray& Route) { double L = 0.0; for (int32 I = 1; I < Route.Num(); ++I) { L += FVector::Dist(Route[I - 1], Route[I]); } return L; } FVector RoutePointM(const TArray& Route, double Dist) { for (int32 I = 1; I < Route.Num(); ++I) { const double Seg = FVector::Dist(Route[I - 1], Route[I]); if (Dist <= Seg || I == Route.Num() - 1) { return FMath::Lerp(Route[I - 1], Route[I], Seg > 1e-9 ? FMath::Clamp(Dist / Seg, 0.0, 1.0) : 0.0); } Dist -= Seg; } return Route.Num() ? Route.Last() : FVector::ZeroVector; } int32 ConsumeFromCounts(TArray& Counts, int32 N) { int32 Taken = 0; while (Taken < N) { int32 Best = INDEX_NONE; for (int32 I = 0; I < Counts.Num(); ++I) { if (Counts[I] > 0 && (Best == INDEX_NONE || Counts[I] < Counts[Best])) { Best = I; } } if (Best == INDEX_NONE) { break; } const int32 Take = FMath::Min(Counts[Best], N - Taken); Counts[Best] -= Take; Taken += Take; } return Taken; } void FPackerState::Step(double Dt, bool bSlotFree, TFunctionRef MakeBox) { // one piece per 0.3 s from the buffer into the partial box of its type; a full partial waits, its pieces stay in the buffer MoveTimer += Dt; while (MoveTimer >= CTune::PackerMoveSec) { MoveTimer -= CTune::PackerMoveSec; for (int32 I = 0; I < Buffer.Num(); ++I) { if (Partial[Buffer[I]] < CTune::BoxSize) { ++Partial[Buffer[I]]; Buffer.RemoveAt(I); break; } } } if (Packing < 0 && bSlotFree) { for (int32 T = 0; T < 3; ++T) { if (Partial[T] >= CTune::BoxSize) { Packing = T; PackTimer = CTune::PackSec; break; } } } if (Packing >= 0) { PackTimer -= Dt; if (PackTimer <= 0.0) { if (bSlotFree) { Partial[Packing] -= CTune::BoxSize; ++BoxesMade; MakeBox(static_cast(Packing), CTune::BoxSize); } Packing = -1; } } } } // ---- the ore box ----------------------------------------------------------------------------------------------------- ADescentOreBox::ADescentOreBox() { PrimaryActorTick.bCanEverTick = false; Mesh = CreateDefaultSubobject(TEXT("Mesh")); SetRootComponent(Mesh); Mesh->SetMobility(EComponentMobility::Movable); Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); // boxes are never physics; the player walks past them Mesh->SetRelativeScale3D(FVector(CTune::BoxX, CTune::BoxY, CTune::BoxZ)); Text = CreateDefaultSubobject(TEXT("Label")); Text->SetupAttachment(Mesh); Text->SetHorizontalAlignment(EHTA_Center); Text->SetVerticalAlignment(EVRTA_TextCenter); Text->SetWorldSize(9.f); Text->SetRelativeLocation(FVector(-51.0, 0.0, 0.0)); Text->SetRelativeRotation(FRotator(0.0, 180.0, 0.0)); Text->SetRelativeScale3D(FVector(1.0 / CTune::BoxX, 1.0 / CTune::BoxY, 1.0 / CTune::BoxZ)); } void ADescentOreBox::Init(int32 InId, uint8 InType, int32 InCount) { BoxId = InId; OreType = FMath::Min(InType, 2); Count = InCount; Mesh->SetStaticMesh(CargoCube()); if (UMaterialInstanceDynamic* MID = CargoSurface(this, CargoOreColour[OreType] * 0.35f, CargoOreColour[OreType], 0.25f)) { Mesh->SetMaterial(0, MID); } UCarrierBlockoutComponent::MakeInterior(Mesh); UCarrierBlockoutComponent::MakeInterior(Text); Text->SetText(FText::FromString(Label())); if (UDescentInteractionRegistry* Reg = UDescentInteractionRegistry::Get(GetWorld())) { Reg->Register(this); } } void ADescentOreBox::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentInteractionRegistry* Reg = UDescentInteractionRegistry::Get(GetWorld())) { Reg->Unregister(this); } if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->ForgetBox(this); } Super::EndPlay(Reason); } FString ADescentOreBox::Label() const { return FString::Printf(TEXT("%s ×%d"), *Descent::Cargo::OreName(OreType).ToString().ToUpper(), Count); } double ADescentOreBox::GetRangeM() const { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (Where.StartsWith(TEXT("arm"))) { return 0.0; } // an arm holds it return (Cargo && Cargo->Carried()) ? 0.0 : CTune::CarryRangeM; // both hands must be free } void ADescentOreBox::Use(ADescentCharacter* User) { if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->PickUp(User, this); } } // ---- the refinery ---------------------------------------------------------------------------------------------------- void ADescentRefinery::InitKit(const Descent::Build::FPlacement& P, FName InKit, int32 InId) { // Blockout: a furnace 3.2 m on a side (the placeable's body) whose front (+X of the actor) has a glowing slit and the hopper at chest height. Super::InitKit(P, InKit, InId); Face = AddPart(TEXT("Slit"), FVector(0.5 * CTune::RefinerySizeM + 0.01, 0.0, CTune::HopperAboveFloorM), FVector(0.03, 1.44, 0.22), FLinearColor::Black, FLinearColor(1.0f, 0.45f, 0.12f), 1.4f); if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->Register(this); } } void ADescentRefinery::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { Cargo->Unregister(this); } Super::EndPlay(Reason); } bool ADescentRefinery::CanPickUp(FString* OutWhyNot) const { if (Queue.Num() > 0 || Reserved > 0) { if (OutWhyNot) { *OutWhyNot = TEXT("The refinery still has ore in its hopper"); } return false; } return true; } FVector ADescentRefinery::HopperWorldUU() const { // front face, chest height (the actor origin is on the floor under the centre of the block) return WorldUUOf(FVector(0.5 * CTune::RefinerySizeM, 0.0, CTune::HopperAboveFloorM)); } bool ADescentRefinery::CanTake(const ADescentOreBox* Box, FString* OutWhyNot) const { if (!Box) { if (OutWhyNot) { *OutWhyNot = TEXT("no box"); } return false; } if (Queue.Num() + Reserved + Box->Count > CTune::RefineryQueueMax) { if (OutWhyNot) { *OutWhyNot = FString::Printf(TEXT("Hopper full: %d queued, room for %d"), Queue.Num(), CTune::RefineryQueueMax - Queue.Num()); } return false; } return true; } FString ADescentRefinery::GetPrompt() const { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (const ADescentOreBox* Box = Cargo ? Cargo->Carried() : nullptr) { return FString::Printf(TEXT("Load %s into the hopper"), *Box->Label()); } return TEXT("Refinery - queue and output (hold E: pick up)"); } void ADescentRefinery::Use(ADescentCharacter* User) { UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (Cargo && Cargo->Carried()) { FString Why; if (!Cargo->LoadHopper(User, this, &Why)) { CargoToast(GetWorld(), Why, EDescentHudToastKind::Warn); } } else if (UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld())) { Ui->Open(EDescentWindow::Refinery); } } void ADescentRefinery::Step(double Dt) { UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); if (Queue.Num() == 0 || !Cargo) { Progress = 0.0; return; } const uint8 T = Queue[0]; Progress = FMath::Min(Progress + Dt, CTune::RefineSec[T]); // the product goes to the carrier's storage; a full storage holds the job at 100 % while (Queue.Num() > 0 && Progress >= CTune::RefineSec[Queue[0]] && Cargo->StorageUsed() < Cargo->StorageCap) { Progress -= CTune::RefineSec[Queue[0]]; ++Cargo->Refined[Queue[0]]; ++Made[Queue[0]]; Queue.RemoveAt(0); if (Queue.Num() == 0) { Progress = 0.0; } else { Progress = FMath::Min(Progress, CTune::RefineSec[Queue[0]]); } } } // ---- the subsystem --------------------------------------------------------------------------------------------------- UDescentCargoSubsystem* UDescentCargoSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentCargoSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Cargo"); StepHandle = Sim->RegisterStep(EDescentSimStep::Cargo, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentCargoSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } ADescentCarrier* UDescentCargoSubsystem::Carrier() const { return UDescentCarrierSubsystem::GetCarrier(GetWorld()); } double UDescentCargoSubsystem::LeadPieceM() const { double Best = -1.0; for (const FPiece& P : Pieces) { Best = FMath::Max(Best, P.Dist); } return Best; } int32 UDescentCargoSubsystem::FreeSlot() const { for (int32 I = 0; I < CTune::OutfeedSlots; ++I) { if (SlotBoxId[I] == 0) { return I; } } return INDEX_NONE; } FVector UDescentCargoSubsystem::SlotLocalM(int32 Slot) const { return FVector(CTune::OutfeedX, CTune::OutfeedY0 + CTune::OutfeedDY * Slot, CTune::CargoFloorZ + CTune::OutfeedAboveFloorM); } ADescentOreBox* UDescentCargoSubsystem::FindBox(int32 Id) const { for (ADescentOreBox* B : BoxList) { if (B && B->BoxId == Id) { return B; } } return nullptr; } void UDescentCargoSubsystem::ForgetBox(ADescentOreBox* Box) { BoxList.Remove(Box); if (Box) { ReleaseSockets(Box->BoxId); } if (CarriedBox.Get() == Box) { CarriedBox.Reset(); } } ADescentOreBox* UDescentCargoSubsystem::SpawnBox(uint8 Type, int32 Count, int32 Slot) { ADescentCarrier* C = Carrier(); UWorld* World = GetWorld(); if (!C || !World) { return nullptr; } if (Slot == -1) { Slot = FreeSlot(); if (Slot == INDEX_NONE) { return nullptr; } } FVector LocalM; FString Where; if (Slot >= 0 && Slot < CTune::OutfeedSlots) { if (SlotBoxId[Slot] != 0) { return nullptr; } LocalM = SlotLocalM(Slot); Where = FString::Printf(TEXT("packer.out%d"), Slot); } else { // on the floor in front of the player (a debug spawn) const APlayerController* PC = World->GetFirstPlayerController(); const APawn* P = PC ? PC->GetPawn() : nullptr; LocalM = P ? C->ToLocalM(P->GetActorLocation() + P->GetActorForwardVector() * 100.0) : FVector(15.0, -6.0, -2.0); const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(LocalM); LocalM.Z = (Room ? Room->FloorZ : LocalM.Z) + 0.5 * CTune::BoxZ; Where = TEXT("floor"); } ADescentOreBox* Box = World->SpawnActor(C->ToWorldUU(LocalM), C->GetActorRotation()); if (!Box) { return nullptr; } Box->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); Box->Init(NextBoxId++, Type, FMath::Clamp(Count, 1, CTune::BoxSize)); Box->Where = Where; if (Slot >= 0 && Slot < CTune::OutfeedSlots) { SlotBoxId[Slot] = Box->BoxId; } BoxList.Add(Box); return Box; } bool UDescentCargoSubsystem::EjectPartials() { bool bAny = false; for (int32 T = 0; T < 3; ++T) { if (Packer.Partial[T] > 0 && SpawnBox(static_cast(T), Packer.Partial[T], -1)) { Packer.Partial[T] = 0; bAny = true; } } return bAny; } bool UDescentCargoSubsystem::PickUp(ADescentCharacter* User, ADescentOreBox* Box) { if (!User || !Box || CarriedBox.IsValid()) { return false; } ReleaseSockets(Box->BoxId); Box->AttachToComponent(User->Camera, FAttachmentTransformRules::KeepWorldTransform); Box->SetActorRelativeLocation(Descent::MetresToUU(FVector(CTune::CarryAheadM, 0.0, -CTune::CarryBelowM))); Box->SetActorRelativeRotation(FRotator(0.0, 90.0, 0.0)); Box->Where = TEXT("hands"); CarriedBox = Box; return true; } bool UDescentCargoSubsystem::PutDown(ADescentCharacter* User, FString* OutWhyNot) { ADescentOreBox* Box = CarriedBox.Get(); ADescentCarrier* C = Carrier(); if (!User || !Box || !C) { return false; } // straight down from the carry point onto the deck: refused over the pool or outside a room const FVector LocalM = C->ToLocalM(User->GetFeetWorldUU() + FRotator(0.0, User->GetControlRotation().Yaw, 0.0).Vector() * Descent::MetresToUU(CTune::PutDownAheadM)); const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(LocalM + FVector(0, 0, 0.3)); namespace BT = Descent::Tune::Carrier; const bool bPool = LocalM.X > BT::PoolX0 - 0.4 && LocalM.X < BT::PoolX1 + 0.4 && LocalM.Y > BT::PoolY0 - 0.4 && LocalM.Y < BT::PoolY1 + 0.4; if (!Room || bPool) { if (OutWhyNot) { *OutWhyNot = TEXT("No room to put the box down here"); } return false; } Box->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); Box->SetActorLocationAndRotation(C->ToWorldUU(FVector(LocalM.X, LocalM.Y, Room->FloorZ + 0.5 * CTune::BoxZ)), FRotator(0.0, User->GetControlRotation().Yaw, 0.0)); Box->Where = TEXT("floor"); CarriedBox.Reset(); return true; } bool UDescentCargoSubsystem::LoadHopper(ADescentCharacter* User, ADescentRefinery* Refinery, FString* OutWhyNot) { ADescentOreBox* Box = CarriedBox.Get(); if (!Box || !Refinery || !Refinery->CanTake(Box, OutWhyNot)) { return false; } for (int32 I = 0; I < Box->Count; ++I) { Refinery->Queue.Add(Box->OreType); } CargoToast(GetWorld(), FString::Printf(TEXT("%s into the hopper"), *Box->Label()), EDescentHudToastKind::Good); CarriedBox.Reset(); Box->Destroy(); // the pieces join the queue, the empty crate disappears return true; } ADescentRefinery* UDescentCargoSubsystem::SpawnRefinery(const FVector& LocalM, double YawDeg) { // M7: the refinery is a placeable (U1); the start one is spawned like a placed one UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); return Build ? Cast(Build->Spawn(TEXT("refinery"), LocalM, YawDeg)) : nullptr; } void UDescentCargoSubsystem::ReleaseSockets(int32 BoxId) { if (BoxId == 0) { return; } for (int32& Id : SlotBoxId) { if (Id == BoxId) { Id = 0; } } for (ADescentBoxRack* R : Racks) { if (R) { for (int32& Id : R->SlotBox) { if (Id == BoxId) { Id = 0; } } } } } ADescentOreBox* UDescentCargoSubsystem::ReleaseCarried() { ADescentOreBox* Box = CarriedBox.Get(); CarriedBox.Reset(); return Box; } void UDescentCargoSubsystem::DropBoxAt(ADescentOreBox* Box, const FVector& LocalM) { ADescentCarrier* C = Carrier(); if (!Box || !C) { return; } const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(LocalM); Box->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); Box->SetActorLocationAndRotation(C->ToWorldUU(FVector(LocalM.X, LocalM.Y, (Room ? Room->FloorZ : LocalM.Z - 0.2) + 0.5 * CTune::BoxZ)), C->GetActorRotation()); Box->Where = TEXT("floor"); } void UDescentCargoSubsystem::DestroyAllBoxes() { TArray> Copy = BoxList; for (ADescentOreBox* B : Copy) { if (B) { B->Destroy(); } } BoxList.Reset(); CarriedBox.Reset(); for (int32& Id : SlotBoxId) { Id = 0; } } ADescentOreBox* UDescentCargoSubsystem::RestoreBox(uint8 Type, int32 Count, const FString& Where, const FVector& LocalM) { if (Where.StartsWith(TEXT("packer.out"))) { return SpawnBox(Type, Count, FCString::Atoi(*Where.RightChop(10))); } ADescentOreBox* Box = SpawnBox(Type, Count, -2); if (!Box) { return nullptr; } if (Where.StartsWith(TEXT("rack"))) { FString Id, Slot; if (Where.RightChop(4).Split(TEXT("."), &Id, &Slot)) { for (ADescentBoxRack* R : Racks) { if (R && R->PlaceId == FCString::Atoi(*Id) && R->PutBox(Box, FCString::Atoi(*Slot))) { return Box; } } } } DropBoxAt(Box, LocalM); return Box; } int32 UDescentCargoSubsystem::CountOre(uint8 Type) const { int32 N = 0; for (const ADescentOreBox* B : BoxList) { if (B && B->OreType == Type) { N += B->Count; } } return N; } int32 UDescentCargoSubsystem::ConsumeOre(uint8 Type, int32 N) { TArray Of; TArray Counts; for (ADescentOreBox* B : BoxList) { if (B && B->OreType == Type) { Of.Add(B); Counts.Add(B->Count); } } const int32 Taken = Descent::Cargo::ConsumeFromCounts(Counts, N); for (int32 I = 0; I < Of.Num(); ++I) { Of[I]->Count = Counts[I]; if (Counts[I] <= 0) { Of[I]->Destroy(); } else { Of[I]->Text->SetText(FText::FromString(Of[I]->Label())); } } return Taken; } double UDescentCargoSubsystem::Debt() const { // the start debt, plus the recovery and implosion fees the vitals keep, minus what was sold double Fees = 0.0; const ADescentPlayerController* PC = GetWorld() ? Cast(GetWorld()->GetFirstPlayerController()) : nullptr; const ADescentCharacter* Ch = PC ? PC->GetDescentCharacter() : nullptr; if (Ch && Ch->Vitals) { Fees = Ch->Vitals->State.Debt; } return CTune::StartDebtCr + Fees - Earned; } bool UDescentCargoSubsystem::SellRefined(uint8 Type, int32 N) { if (Type > 2 || N <= 0 || Refined[Type] < N) { return false; } Refined[Type] -= N; Earned += static_cast(CTune::RefinedValue[Type]) * N; Sold += N; return true; } bool UDescentCargoSubsystem::SellBox(int32 BoxId) { ADescentOreBox* Box = FindBox(BoxId); if (!Box || CarriedBox.Get() == Box) { return false; } Earned += static_cast(CTune::OreValue[Box->OreType]) * Box->Count; Sold += Box->Count; Box->Destroy(); // collected by the wall chute return true; } void UDescentCargoSubsystem::AbortUnload() { UnloadTender.Reset(); // pieces already in the pipe keep going; the rest stays in the hold bFeedPaused = false; } void UDescentCargoSubsystem::StepUnload(double Dt) { ADescentCarrier* C = Carrier(); UCarrierCarouselComponent* Car = C ? C->Carousel.Get() : nullptr; FullToastTimer = FMath::Max(0.0, FullToastTimer - Dt); AKazahTender* T = UnloadTender.Get(); if (!T) { // a Tender that hangs at BOARD with ore in its hold starts to unload (section 6.3: when a dock completes) AKazahTender* AtBoard = Car ? Car->TenderAtBoard() : nullptr; if (AtBoard && AtBoard->Flight->HoldOrder.Num() > 0) { UnloadTender = AtBoard; UnloadPhase = 0; PhaseTimer = CTune::CoupleSec; FeedTimer = 0.0; UnloadedCount = 0; } return; } if (T->Flight->State != ETenderState::Cradle) { AbortUnload(); return; } if (UnloadPhase == 0) { PhaseTimer -= Dt; if (PhaseTimer <= 0.0) { UnloadPhase = 1; } } else if (UnloadPhase == 1) { if (T->Flight->HoldOrder.Num() == 0) { if (Pieces.Num() == 0) { UnloadPhase = 2; PhaseTimer = CTune::DecoupleSec; } return; } bFeedPaused = !Packer.CanAccept(Pieces.Num()); if (bFeedPaused) { if (FullToastTimer <= 0.0) { CargoToast(GetWorld(), TEXT("PACKER FULL — clear the outfeed"), EDescentHudToastKind::Warn); FullToastTimer = CTune::PackerFullToastEverySec; } return; } FeedTimer -= Dt; if (FeedTimer <= 0.0) { // last in, first out: the cargo display empties from the top FeedTimer = CTune::FeedEverySec; const int32 Type = Descent::Cargo::OreTypeOf(T->Flight->HoldOrder.Pop()); FPiece P; P.Type = static_cast(FMath::Max(0, Type)); Pieces.Add(P); ++UnloadedCount; } } else { PhaseTimer -= Dt; if (PhaseTimer <= 0.0) { CargoToast(GetWorld(), FString::Printf(TEXT("Unloaded %d pieces"), UnloadedCount), EDescentHudToastKind::Good); AbortUnload(); } } } void UDescentCargoSubsystem::Step(double Dt) { if (!Carrier()) { return; } StepUnload(Dt); // pieces travel the pipe at 4 m/s and drop into the packer's buffer const double Length = Descent::Cargo::RouteLengthM(Descent::Cargo::OrePipeRouteM()); for (int32 I = Pieces.Num() - 1; I >= 0; --I) { Pieces[I].Dist += CTune::PieceSpeedMps * Dt; if (Pieces[I].Dist >= Length) { Packer.Buffer.Add(Pieces[I].Type); Pieces.RemoveAt(I); } } Packer.Step(Dt, FreeSlot() != INDEX_NONE, [this](uint8 Type, int32 Count) { SpawnBox(Type, Count, -1); }); for (ADescentRefinery* R : Refineries) { if (R) { R->Step(Dt); } } for (ADescentRobotArm* A : Arms) { if (A) { A->StepArm(Dt); } } UpdateVisuals(); WindowTimer -= Dt; if (WindowTimer <= 0.0) { WindowTimer = 0.25; FillWindows(); } } void UDescentCargoSubsystem::EnsureVisuals() { ADescentCarrier* C = Carrier(); if (PipeActor || !C) { return; } PipeActor = GetWorld()->SpawnActor(); USceneComponent* Root = NewObject(PipeActor, TEXT("Root")); Root->SetMobility(EComponentMobility::Movable); PipeActor->SetRootComponent(Root); Root->RegisterComponent(); PipeActor->SetActorTransform(C->GetActorTransform()); PipeActor->AttachToActor(C, FAttachmentTransformRules::KeepWorldTransform); // U13: the transparent pipe, one glass segment per leg of the route UMaterialInterface* Glass = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentGlass.M_DescentGlass"), nullptr, LOAD_NoWarn); const TArray& Route = Descent::Cargo::OrePipeRouteM(); for (int32 I = 1; I < Route.Num(); ++I) { const FVector A = Route[I - 1], B = Route[I]; UStaticMeshComponent* Seg = NewObject(PipeActor, *FString::Printf(TEXT("Pipe_%d"), I)); Seg->SetMobility(EComponentMobility::Movable); Seg->SetupAttachment(Root); Seg->SetStaticMesh(CargoCube()); Seg->SetCollisionEnabled(ECollisionEnabled::NoCollision); Seg->SetCastShadow(false); Seg->SetRelativeLocation(Descent::MetresToUU(0.5 * (A + B))); Seg->SetRelativeRotation((B - A).Rotation()); Seg->SetRelativeScale3D(FVector(FVector::Dist(A, B) + CTune::PipeSizeM, CTune::PipeSizeM, CTune::PipeSizeM)); if (Glass) { UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Glass, PipeActor); MID->SetScalarParameterValue(TEXT("OpacityFaceOn"), 0.16f); MID->SetScalarParameterValue(TEXT("OpacityGrazing"), 0.55f); Seg->SetMaterial(0, MID); } Seg->SetLightingChannels(false, true, false); Seg->RegisterComponent(); } for (int32 T = 0; T < 3; ++T) { UInstancedStaticMeshComponent* M = NewObject(PipeActor, *FString::Printf(TEXT("Pieces_%d"), T)); M->SetMobility(EComponentMobility::Movable); M->SetupAttachment(Root); M->SetStaticMesh(CargoCube()); M->SetCollisionEnabled(ECollisionEnabled::NoCollision); M->SetCastShadow(false); // emissive ore colour x 2, so a piece glows through the glass if (UMaterialInstanceDynamic* MID = CargoSurface(PipeActor, CargoOreColour[T] * 0.3f, CargoOreColour[T], 2.0f)) { M->SetMaterial(0, MID); } UCarrierBlockoutComponent::MakeInterior(M); M->RegisterComponent(); PieceMeshes.Add(M); } } void UDescentCargoSubsystem::UpdateVisuals() { EnsureVisuals(); if (PieceMeshes.Num() < 3) { return; } for (UInstancedStaticMeshComponent* M : PieceMeshes) { M->ClearInstances(); } const TArray& Route = Descent::Cargo::OrePipeRouteM(); for (const FPiece& P : Pieces) { PieceMeshes[P.Type]->AddInstance(FTransform(FRotator(P.Dist * 40.0, P.Dist * 25.0, 0.0), Descent::MetresToUU(Descent::Cargo::RoutePointM(Route, P.Dist)), FVector(CTune::PieceSizeM)), /*bWorldSpace*/ false); } } void UDescentCargoSubsystem::FillWindows() { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); if (!Ui || !Ui->IsWindowOpen()) { return; } const EDescentWindow W = Ui->GetWindow(); TWeakObjectPtr Weak(this); if (W == EDescentWindow::Refinery) { FDescentRefineryViewModel& M = Ui->Models().Refinery; const ADescentRefinery* R = Refineries.Num() ? Refineries[0].Get() : nullptr; M.Queue.Reset(); if (R) { int32 N[3] = { 0, 0, 0 }; for (uint8 T : R->Queue) { ++N[T]; } for (int32 T = 0; T < 3; ++T) { if (N[T] > 0) { M.Queue.Add({ Descent::Cargo::OreName(static_cast(T)), N[T] }); } } M.CurrentOre = R->Queue.Num() ? Descent::Cargo::OreName(R->Queue[0]) : NAME_None; M.Progress = R->Queue.Num() ? static_cast(R->Progress / CTune::RefineSec[R->Queue[0]]) : 0.f; M.SecondsLeft = R->Queue.Num() ? static_cast(CTune::RefineSec[R->Queue[0]] - R->Progress) : 0.f; } M.Ingots = Refined[0]; M.Wafers = Refined[1]; M.Lattices = Refined[2]; M.bStorageFull = StorageUsed() >= StorageCap; Ui->NotifyChanged(); } else if (W == EDescentWindow::Packer) { FDescentPackerViewModel& M = Ui->Models().Packer; M.Buffer = Packer.Buffer.Num(); for (int32 T = 0; T < 3; ++T) { M.Partial[T] = Packer.Partial[T]; } for (int32 I = 0; I < CTune::OutfeedSlots; ++I) { const ADescentOreBox* B = FindBox(SlotBoxId[I]); M.Slots[I].OreId = B ? Descent::Cargo::OreName(B->OreType) : NAME_None; M.Slots[I].Count = B ? B->Count : 0; } M.StatusText = bFeedPaused ? TEXT("PACKER FULL — clear the outfeed") : (IsUnloading() ? TEXT("Unloading") : TEXT("Idle")); M.OnEject = [Weak]() { if (UDescentCargoSubsystem* S = Weak.Get()) { S->EjectPartials(); } }; Ui->NotifyChanged(); } else if (W == EDescentWindow::Terminal) { static const TCHAR* const Names[3] = { TEXT("Ferrite Ingot"), TEXT("Cobalt Wafer"), TEXT("Pearl Lattice") }; FDescentTerminalViewModel& M = Ui->Models().Terminal; M.Refined.Reset(); M.Boxes.Reset(); for (int32 T = 0; T < 3; ++T) { FDescentSellRow Row; Row.ItemId = Descent::Cargo::RefinedName(static_cast(T)); Row.Name = Names[T]; Row.Count = Refined[T]; Row.UnitValue = CTune::RefinedValue[T]; Row.OnSellOne = [Weak, T]() { if (UDescentCargoSubsystem* S = Weak.Get()) { S->SellRefined(static_cast(T), 1); } }; Row.OnSellAll = [Weak, T]() { if (UDescentCargoSubsystem* S = Weak.Get()) { S->SellRefined(static_cast(T), S->Refined[T]); } }; M.Refined.Add(Row); } for (const ADescentOreBox* B : BoxList) { if (!B || CarriedBox.Get() == B) { continue; } FDescentSellRow Row; Row.ItemId = FName(*FString::Printf(TEXT("box:%d"), B->BoxId)); Row.Name = B->Label(); Row.Count = B->Count; Row.UnitValue = CTune::OreValue[B->OreType] * B->Count; const int32 Id = B->BoxId; Row.OnSellAll = [Weak, Id]() { if (UDescentCargoSubsystem* S = Weak.Get()) { S->SellBox(Id); } }; Row.OnSellOne = Row.OnSellAll; M.Boxes.Add(Row); } M.Debt = static_cast(Debt()); M.Earned = static_cast(Earned); M.Sold = Sold; Ui->NotifyChanged(); } } // ---- boot: the start refinery (section 3.4), on D2 in ORE PROCESSING, facing +y --------------------------------------------- DESCENT_BOOT_STEP(SpawnStartRefinery, Descent::Boot::Order::SpawnStartPlaceables) { UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(Ctx.World); if (!Cargo || !Cargo->SpawnRefinery(FVector(CTune::RefineryX, CTune::RefineryY, Descent::Tune::Carrier::DeckFloorZ2), 90.0)) { Ctx.Fail(TEXT("could not spawn the start refinery")); } } // ---- state sections `cargo`, `store`, `econ`, `refineries` (section 9.4) ---------------------------------------------------- static FDescentStateProviderRegistrar GCargoStateSection(TEXT("cargo"), [](UWorld* World, FJsonObject& Out) { const UDescentCargoSubsystem* S = UDescentCargoSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } Out.SetBoolField(TEXT("unloading"), S->IsUnloading()); Out.SetBoolField(TEXT("feed_paused"), S->bFeedPaused); Out.SetNumberField(TEXT("pipe_pieces"), S->NumPipePieces()); Out.SetNumberField(TEXT("pipe_length_m"), Descent::Json::Round(Descent::Cargo::RouteLengthM(Descent::Cargo::OrePipeRouteM()))); TSharedRef P = MakeShared(); P->SetNumberField(TEXT("buffer"), S->Packer.Buffer.Num()); P->SetArrayField(TEXT("partial"), { MakeShared(S->Packer.Partial[0]), MakeShared(S->Packer.Partial[1]), MakeShared(S->Packer.Partial[2]) }); Out.SetObjectField(TEXT("packer"), P); TArray> Boxes; for (const ADescentOreBox* B : S->Boxes()) { if (!B) { continue; } TSharedRef J = MakeShared(); J->SetNumberField(TEXT("id"), B->BoxId); J->SetStringField(TEXT("type"), Descent::Cargo::OreName(B->OreType).ToString()); J->SetNumberField(TEXT("count"), B->Count); J->SetStringField(TEXT("where"), B->Where); Boxes.Add(MakeShared(J)); } Out.SetArrayField(TEXT("boxes"), Boxes); TArray> Arms, Racks; for (const ADescentRobotArm* A : S->Arms) { if (!A) { continue; } TSharedRef J = MakeShared(); J->SetNumberField(TEXT("id"), A->PlaceId); J->SetNumberField(TEXT("mk"), A->Mk); J->SetBoolField(TEXT("power"), A->bPower); J->SetNumberField(TEXT("moved"), A->Moved); J->SetBoolField(TEXT("moving"), A->IsMoving()); J->SetNumberField(TEXT("cycle"), Descent::Json::Round(A->CycleFraction())); J->SetStringField(TEXT("status"), A->Status); Arms.Add(MakeShared(J)); } for (const ADescentBoxRack* R : S->Racks) { if (!R) { continue; } TSharedRef J = MakeShared(); J->SetNumberField(TEXT("id"), R->PlaceId); J->SetNumberField(TEXT("boxes"), R->NumBoxes()); Racks.Add(MakeShared(J)); } Out.SetArrayField(TEXT("arms"), Arms); Out.SetArrayField(TEXT("racks"), Racks); Out.SetField(TEXT("carrying"), S->Carried() ? StaticCastSharedRef(MakeShared(S->Carried()->BoxId)) : StaticCastSharedRef(MakeShared())); }); static FDescentStateProviderRegistrar GStoreStateSection(TEXT("store"), [](UWorld* World, FJsonObject& Out) { const UDescentCargoSubsystem* S = UDescentCargoSubsystem::Get(World); if (!S) { return; } Out.SetNumberField(TEXT("ingot"), S->Refined[0]); Out.SetNumberField(TEXT("wafer"), S->Refined[1]); Out.SetNumberField(TEXT("lattice"), S->Refined[2]); Out.SetNumberField(TEXT("used"), S->StorageUsed()); Out.SetNumberField(TEXT("cap"), S->StorageCap); TSharedRef L = MakeShared(); for (uint8 T = 0; T < 3; ++T) { L->SetNumberField(Descent::Cargo::OreName(T).ToString(), S->CountOre(T)); } Out.SetObjectField(TEXT("ore_ledger"), L); }); static FDescentStateProviderRegistrar GEconStateSection(TEXT("econ"), [](UWorld* World, FJsonObject& Out) { const UDescentCargoSubsystem* S = UDescentCargoSubsystem::Get(World); Out.SetNumberField(TEXT("debt"), S ? Descent::Json::Round(S->Debt()) : 0.0); Out.SetNumberField(TEXT("earned"), S ? Descent::Json::Round(S->Earned) : 0.0); Out.SetNumberField(TEXT("sold"), S ? S->Sold : 0); }); static FDescentStateProviderRegistrar GRefineriesStateSection(TEXT("refineries"), [](UWorld* World, FJsonObject& Out) { const UDescentCargoSubsystem* S = UDescentCargoSubsystem::Get(World); TArray> List; if (S) { for (const ADescentRefinery* R : S->GetRefineries()) { if (!R) { continue; } TSharedRef J = MakeShared(); J->SetNumberField(TEXT("id"), R->RefineryId); J->SetNumberField(TEXT("queue"), R->Queue.Num()); J->SetStringField(TEXT("current"), R->Queue.Num() ? Descent::Cargo::OreName(R->Queue[0]).ToString() : FString()); J->SetNumberField(TEXT("progress"), Descent::Json::Round(R->Progress)); J->SetNumberField(TEXT("made"), R->Made[0] + R->Made[1] + R->Made[2]); J->SetStringField(TEXT("room"), R->RoomId().ToString()); J->SetField(TEXT("pos_m"), Descent::Json::Vec3(R->Place.PosM)); List.Add(MakeShared(J)); } } Out.SetArrayField(TEXT("items"), List); });