// Milestone M7 scenarios (DESIGN_UE.md section 10, "Build and craft"): m7_build (U1), m7_arms (U14), m7_craft (V8), m7_inventory (V11), m7_save. #include "Debug/DescentScenarioUtil.h" #include "Core/DescentSimSubsystem.h" #include "Player/DescentInteraction.h" #include "Systems/DescentAutomation.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCargo.h" #include "Systems/DescentCraft.h" #include "Systems/DescentSave.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/CarrierLayout.h" #include "Vehicles/TenderFlight.h" #include "World/DescentOreSubsystem.h" #include "World/DescentTerrainSubsystem.h" #include "World/DescentWorldScenarioSteps.h" #include "DescentUnits.h" namespace { using namespace Descent::Scn; UDescentBuildSubsystem* M7Build(const FDescentScenarioContext& C) { return UDescentBuildSubsystem::Get(C.World); } UDescentCargoSubsystem* M7Cargo(const FDescentScenarioContext& C) { return UDescentCargoSubsystem::Get(C.World); } UDescentCraftSubsystem* M7Craft(const FDescentScenarioContext& C) { return UDescentCraftSubsystem::Get(C.World); } struct FM7Mark { int32 A = 0, B = 0; double V = 0.0; TArray Text; }; } DESCENT_SCENARIO(m7_build, "U1: a table kit as a ghost in the habitation room is valid (green); in front of a door it is KEEP_OUT (red); Place adds it and takes the kit; the walking player is stopped by it; Pickup gives the kit back; the refinery is picked up and placed in the cargo bay.") { S.TimeLimit(300.f); S.Exec(TEXT("Descent.Player.Place hab")); S.Exec(TEXT("Descent.Build.Give table 1")); S.WaitSim(0.3); // the real path: the ghost follows the eye ray S.Exec(TEXT("Descent.Player.PlaceLocal 17.5 -3.75 1.5 180")); S.Exec(TEXT("Descent.Player.Look 180 -35")); S.Exec(TEXT("Descent.Build.Ghost kit_table")); S.WaitSim(0.4); TSharedRef M = MakeShared(); S.Expect(TEXT("ghost.valid is true on the habitation floor"), [M](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); M->A = B->Placed().Num(); UE_LOG(LogDescent, Display, TEXT("M7 build: ghost %s at (%.2f, %.2f, %.2f), valid %d, reason '%s', kits %d, placed %d"), *B->GhostKit.ToString(), B->GhostPlace.PosM.X, B->GhostPlace.PosM.Y, B->GhostPlace.PosM.Z, B->bGhostValid, *B->GhostReason, B->KitCount(TEXT("table")), M->A); return B->IsGhosting() && B->bGhostValid; }); HudShot(S, TEXT("ghost_valid")); // in front of a door of the room: red, KEEP_OUT S.Do(TEXT("aim the ghost 0.75 m inside a door of the habitation room"), [](FDescentScenarioContext& C) { const Descent::Carrier::FLayout& L = Descent::Carrier::FLayout::Get(); const Descent::Carrier::FRoomDef* Hab = L.FindRoom(TEXT("hab")); for (const Descent::Carrier::FDoorDef& D : L.Doors) { const Descent::Carrier::FRoomDef* A = nullptr; const Descent::Carrier::FRoomDef* B = nullptr; if (!L.DoorRooms(D, A, B) || (A != Hab && B != Hab) || D.WallAtConstX()) { continue; } const FVector At(D.Mid().X, D.Mid().Y - 0.75, Hab->FloorZ); M7Build(C)->SetDebugAim(At, FVector::UpVector); UE_LOG(LogDescent, Display, TEXT("M7 build: door at (%.2f, %.2f), ghost aimed at (%.2f, %.2f)"), D.Mid().X, D.Mid().Y, At.X, At.Y); return; } C.Fail(TEXT("no door of the habitation room on a wall along x")); }); S.WaitSim(0.3); S.Expect(TEXT("ghost.valid is false with reason KEEP_OUT"), [](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); UE_LOG(LogDescent, Display, TEXT("M7 build: at the door valid %d, reason '%s'"), B->bGhostValid, *B->GhostReason); return !B->bGhostValid && B->GhostReason == TEXT("KEEP_OUT"); }); S.Do(TEXT("look at the red ghost"), [](FDescentScenarioContext& C) { const FVector G = M7Build(C)->GhostPlace.CentreM; const ADescentCarrier* K = Carrier(C); C.Debug->PlaceCamera(K->ToWorldUU(G + FVector(2.4, -2.6, 1.3)) / 100.0, (K->ToWorldUU(G) - K->ToWorldUU(G + FVector(2.4, -2.6, 1.3))).Rotation(), 70.f); }); S.Shot(TEXT("ghost_invalid")); ViewPlayer(S); // back to the eye ray, and place S.Exec(TEXT("Descent.Build.Ghost kit_table")); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Place")); S.Expect(TEXT("Place: build.placed +1, the table kit -1"), [M](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); const ADescentPlaceable* T = B->FindFirst(TEXT("table")); UE_LOG(LogDescent, Display, TEXT("M7 build: placed %d (was %d), table kits %d, table at (%.2f, %.2f) in %s"), B->Placed().Num(), M->A, B->KitCount(TEXT("table")), T ? T->Place.PosM.X : 0.0, T ? T->Place.PosM.Y : 0.0, T ? *T->RoomId().ToString() : TEXT("-")); return B->Placed().Num() == M->A + 1 && B->KitCount(TEXT("table")) == 0 && T && T->RoomId() == TEXT("hab"); }); // walk at it S.Do(TEXT("walk straight at the table"), [M](FDescentScenarioContext& C) { const ADescentPlaceable* T = M7Build(C)->FindFirst(TEXT("table")); M->V = T ? T->Place.CentreM.X + T->Place.HalfM.X : 0.0; FDescentIntent I; I.Move = FVector(1.0, 0.0, 0.0); C.Sim()->SetDebugIntent(I, 4.0); }); S.WaitSim(4.3); S.Expect(TEXT("the walking player is stopped by the table"), [M](FDescentScenarioContext& C) { const FVector F = FeetM(C); UE_LOG(LogDescent, Display, TEXT("M7 build: after 4 s of walking at the table the feet are at x %.2f (started 17.50, the table's near face is at %.2f)"), F.X, M->V); return F.X < 17.0 && F.X > M->V + 0.15; }); HudShot(S, TEXT("table_placed")); S.Exec(TEXT("Descent.Build.Pickup table")); S.Expect(TEXT("Pickup: the kit is back, the object is gone"), [M](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); return B->KitCount(TEXT("table")) == 1 && B->Placed().Num() == M->A; }); // the hold on E (0.8 s) is the player's way to pick up: prove it on the start fabricator's neighbour, a chair placed for it S.Exec(TEXT("Descent.Build.Give chair 1")); S.Exec(TEXT("Descent.Player.PlaceLocal 17.5 -3.75 1.5 180")); S.Exec(TEXT("Descent.Player.Look 180 -35")); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Ghost chair")); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Place")); S.Exec(TEXT("Descent.Player.PlaceLocal 17.0 -3.75 1.5 180")); S.Exec(TEXT("Descent.Player.Look 180 -33")); S.WaitSim(0.4); S.Do(TEXT("hold E for 1 s"), [](FDescentScenarioContext& C) { ADescentPlayerController* P = PC(C); UE_LOG(LogDescent, Display, TEXT("M7 build: the prompt before the hold is '%s'"), *Char(C)->Interaction->GetFocusLabel()); P->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::E, IE_Pressed, 1.f)); }); S.WaitSim(1.1); S.Do(TEXT("release E"), [](FDescentScenarioContext& C) { PC(C)->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::E, IE_Released, 0.f)); }); S.Expect(TEXT("holding E for 0.8 s picked the chair up"), [](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); UE_LOG(LogDescent, Display, TEXT("M7 build: after the hold chair kits %d, chairs placed %d"), B->KitCount(TEXT("chair")), B->FindFirst(TEXT("chair")) ? 1 : 0); return B->KitCount(TEXT("chair")) == 1 && !B->FindFirst(TEXT("chair")); }); // the refinery moves to the cargo bay S.Exec(TEXT("Descent.Build.Pickup refinery")); S.Expect(TEXT("the empty refinery became a kit"), [](FDescentScenarioContext& C) { return M7Build(C)->KitCount(TEXT("refinery")) == 1 && M7Cargo(C)->GetRefineries().Num() == 0; }); S.Exec(TEXT("Descent.Build.Ghost refinery")); S.Exec(FString::Printf(TEXT("Descent.Build.Aim 10.5 -4.75 %.2f"), Descent::Tune::Carrier::DeckFloorZ2)); S.WaitSim(0.3); S.Expect(TEXT("the refinery ghost is valid in the cargo bay"), [](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); UE_LOG(LogDescent, Display, TEXT("M7 build: refinery ghost valid %d, reason '%s'"), B->bGhostValid, *B->GhostReason); return B->bGhostValid; }); S.Exec(TEXT("Descent.Build.Place")); S.Expect(TEXT("refineries[0].room == cargo"), [](FDescentScenarioContext& C) { const UDescentCargoSubsystem* Cargo = M7Cargo(C); const ADescentRefinery* R = Cargo->GetRefineries().Num() ? Cargo->GetRefineries()[0].Get() : nullptr; UE_LOG(LogDescent, Display, TEXT("M7 build: refineries %d, room %s"), Cargo->GetRefineries().Num(), R ? *R->RoomId().ToString() : TEXT("-")); return R && R->RoomId() == TEXT("cargo"); }); LocalShot(S, TEXT("cargo_rearranged"), FVector(18.6, -2.4, -0.2), FVector(11.0, -5.5, -2.2), 85.f); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m7_arms, "U14: the refinery stands 2.5 m from the packer outfeed with a Handling Arm Mk I between them; two boxes on the outfeed are carried to the hopper by the arm alone: 12 pieces in the refinery, cargo.arms[0].moved == 2.") { S.TimeLimit(300.f); S.Exec(TEXT("Descent.Player.Place corr2")); S.Exec(TEXT("Descent.Build.Pickup refinery")); S.Exec(TEXT("Descent.Build.Give arm1 1")); const double Z2 = Descent::Tune::Carrier::DeckFloorZ2; // placed the player's way: ghost, aim, confirm (the validity rules apply) S.Exec(TEXT("Descent.Build.Ghost refinery")); S.Exec(FString::Printf(TEXT("Descent.Build.Aim 12.75 -5.0 %.2f"), Z2)); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Place")); S.Exec(TEXT("Descent.Build.Ghost arm1")); S.Exec(FString::Printf(TEXT("Descent.Build.Aim 15.0 -5.0 %.2f"), Z2)); S.WaitSim(0.3); S.Expect(TEXT("the arm ghost is valid between the refinery and the outfeed"), [](FDescentScenarioContext& C) { const UDescentBuildSubsystem* B = M7Build(C); UE_LOG(LogDescent, Display, TEXT("M7 arms: arm ghost valid %d, reason '%s'"), B->bGhostValid, *B->GhostReason); return B->bGhostValid; }); S.Exec(TEXT("Descent.Build.Place")); S.Expect(TEXT("the hopper is 2.5 m (within 0.3) from the outfeed and inside the arm's reach"), [](FDescentScenarioContext& C) { const UDescentCargoSubsystem* Cargo = M7Cargo(C); if (Cargo->GetRefineries().Num() != 1 || Cargo->Arms.Num() != 1) { return false; } const FVector H = Carrier(C)->ToLocalM(Cargo->GetRefineries()[0]->HopperWorldUU()); const double Gap = Descent::Tune::Cargo::OutfeedX - H.X; UE_LOG(LogDescent, Display, TEXT("M7 arms: hopper at (%.2f, %.2f, %.2f), %.2f m from the outfeed; arm reach %.1f m, cycle %.1f s"), H.X, H.Y, H.Z, Gap, Cargo->Arms[0]->ReachM, Cargo->Arms[0]->CycleSec); return FMath::Abs(Gap - 2.5) <= 0.3; }); S.Exec(TEXT("Descent.Cargo.SpawnBox ferrite 6")); S.Exec(TEXT("Descent.Cargo.SpawnBox ferrite 6")); S.Until(TEXT("the arm is in mid-swing with a box"), [](FDescentScenarioContext& C) { const UDescentCargoSubsystem* Cargo = M7Cargo(C); if (C.SecondsInStep() > 30.0) { C.Fail(FString::Printf(TEXT("the arm never started: status '%s'"), Cargo->Arms.Num() ? *Cargo->Arms[0]->Status : TEXT("no arm"))); return true; } return Cargo->Arms.Num() && Cargo->Arms[0]->IsMoving() && Cargo->Arms[0]->CycleFraction() > 0.45 && Cargo->Arms[0]->CycleFraction() < 0.6; }); S.Do(TEXT("log the status"), [](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("M7 arms: status '%s' at %.0f %% of the cycle"), *M7Cargo(C)->Arms[0]->Status, 100.0 * M7Cargo(C)->Arms[0]->CycleFraction()); }); LocalShot(S, TEXT("arm_moving"), FVector(17.6, -1.9, -0.6), FVector(15.0, -5.0, -1.6), 80.f); S.FastForward(20.0); S.Expect(TEXT("FastForward 20: cargo.arms[0].moved == 2 and all 12 pieces went into the refinery"), [](FDescentScenarioContext& C) { const UDescentCargoSubsystem* Cargo = M7Cargo(C); const ADescentRefinery* R = Cargo->GetRefineries()[0]; const int32 Made = R->Made[0] + R->Made[1] + R->Made[2]; UE_LOG(LogDescent, Display, TEXT("M7 arms: moved %d, refinery queue %d + made %d, boxes left %d, status '%s'"), Cargo->Arms[0]->Moved, R->Queue.Num(), Made, Cargo->Boxes().Num(), *Cargo->Arms[0]->Status); return Cargo->Arms[0]->Moved == 2 && R->Queue.Num() + Made == 12 && Cargo->Boxes().Num() == 0; }); // a rack as a buffer: with the hopper full a third and fourth box wait on the rack, and never come back S.Exec(TEXT("Descent.Build.Give rack 1")); S.Exec(TEXT("Descent.Build.Ghost rack")); S.Exec(FString::Printf(TEXT("Descent.Build.Aim 15.0 -7.25 %.2f"), Z2)); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Place")); S.Exec(TEXT("Descent.Cargo.SpawnBox cobalt 6")); S.FastForward(12.0); S.Expect(TEXT("the hopper has no room for 6 more: the box waits on the rack (rank 1)"), [](FDescentScenarioContext& C) { const UDescentCargoSubsystem* Cargo = M7Cargo(C); const int32 OnRack = Cargo->Racks.Num() ? Cargo->Racks[0]->NumBoxes() : -1; UE_LOG(LogDescent, Display, TEXT("M7 arms: racks %d, boxes on the rack %d, moved %d, refinery queue %d, status '%s'"), Cargo->Racks.Num(), OnRack, Cargo->Arms[0]->Moved, Cargo->GetRefineries()[0]->Queue.Num(), *Cargo->Arms[0]->Status); return OnRack == 1 && Cargo->Arms[0]->Moved == 3; }); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m7_craft, "V8: Depth Module Mk I raises the Tender rating to 2,700 m and takes exactly 3 ingots and 2 wafers; the Hull Patch is refused while the carrier hull is whole and built when it is damaged; the fabricator window (opened with E at the placed fabricator) shows the locked weapon row.") { S.TimeLimit(240.f); S.Exec(TEXT("Descent.Store.Give ingot 20")); S.Exec(TEXT("Descent.Store.Give wafer 10")); S.Exec(TEXT("Descent.Craft depth1")); S.Expect(TEXT("tenders[0].rating_m == 2700; storage 17 ingots, 8 wafers"), [](FDescentScenarioContext& C) { const UDescentTenderFleet* F = UDescentTenderFleet::Get(C.World); const double Rating = F->At(0)->Flight->Stats().Rating; UE_LOG(LogDescent, Display, TEXT("M7 craft: rating %.0f m, ingots %d, wafers %d"), Rating, M7Cargo(C)->Refined[0], M7Cargo(C)->Refined[1]); return FMath::Abs(Rating - 2700.0) < 0.5 && M7Cargo(C)->Refined[0] == 17 && M7Cargo(C)->Refined[1] == 8; }); S.FastForward(3.0); S.Expect(TEXT("Hull Patch is refused at 100 % hull; Depth Mk I cannot be built twice; the harpoon is locked"), [](FDescentScenarioContext& C) { FString A, B, D; const bool bPatch = M7Craft(C)->CanCraft(TEXT("hull_patch"), &A), bAgain = M7Craft(C)->CanCraft(TEXT("depth1"), &B), bGun = M7Craft(C)->CanCraft(TEXT("harpoon"), &D); UE_LOG(LogDescent, Display, TEXT("M7 craft: patch %d '%s'; depth1 again %d '%s'; harpoon %d '%s'"), bPatch, *A, bAgain, *B, bGun, *D); return !bPatch && !bAgain && !bGun; }); S.Exec(TEXT("Descent.Carrier.SetHull 60")); S.Exec(TEXT("Descent.Craft hull_patch")); S.Expect(TEXT("with the hull at 60 the patch is built: hull 85, 13 ingots, 7 wafers"), [](FDescentScenarioContext& C) { UE_LOG(LogDescent, Display, TEXT("M7 craft: carrier hull %.1f, ingots %d, wafers %d"), Carrier(C)->Helm.Hull, M7Cargo(C)->Refined[0], M7Cargo(C)->Refined[1]); return FMath::Abs(Carrier(C)->Helm.Hull - 85.0) < 0.01 && M7Cargo(C)->Refined[0] == 13 && M7Cargo(C)->Refined[1] == 7; }); S.FastForward(3.0); // a kit from the BUILD tab S.Exec(TEXT("Descent.Craft kit_locker")); S.Expect(TEXT("the Storage Locker recipe gave one kit for 4 ingots"), [](FDescentScenarioContext& C) { return UDescentBuildSubsystem::Get(C.World)->KitCount(TEXT("locker")) == 1 && M7Cargo(C)->Refined[0] == 9; }); // the window, through the placed fabricator and the E key S.Exec(TEXT("Descent.Player.PlaceLocal -1 -5.0 1.5 -90")); S.Exec(TEXT("Descent.Player.Look -90 -8")); S.WaitSim(0.4); S.Exec(TEXT("Descent.Player.Use")); S.WaitSim(0.3); S.Expect(TEXT("E at the fabricator opens its window"), [](FDescentScenarioContext& C) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(C.World); UE_LOG(LogDescent, Display, TEXT("M7 craft: prompt '%s', window %s, categories %d"), *Char(C)->Interaction->GetFocusLabel(), *Descent::Ui::WindowName(Ui->GetWindow()).ToString(), Ui->Models().Fabricator.Categories.Num()); return Ui->GetWindow() == EDescentWindow::Fabricator && Ui->Models().Fabricator.Categories.Num() == 6; }); S.Do(TEXT("the WEAPONS tab"), [](FDescentScenarioContext& C) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(C.World); if (!Ui->SetTab(TEXT("weapons"))) { UE_LOG(LogDescent, Display, TEXT("M7 craft: no tab named WEAPONS; tabs: %d"), Ui->GetTabs().Num()); } }); S.Expect(TEXT("the locked weapon row is in the model"), [](FDescentScenarioContext& C) { const FDescentFabricatorViewModel& M = UDescentUiSubsystem::Get(C.World)->Models().Fabricator; for (const FDescentFabricatorCategory& Cat : M.Categories) { for (const FDescentRecipeRow& R : Cat.Rows) { if (R.Id == TEXT("harpoon")) { return R.bLocked && R.LockedReason.Contains(TEXT("licensed")); } } } return false; }); HudShot(S, TEXT("fabricator")); S.Do(TEXT("close"), [](FDescentScenarioContext& C) { UDescentUiSubsystem::Get(C.World)->Close(); }); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m7_inventory, "V11: the PDA inventory shows the 24 cells and the greyed weapon slot; a flare is put on hotbar slot 4 from the PDA.") { S.TimeLimit(180.f); S.Exec(TEXT("Descent.Inventory.Give flare 5")); S.Do(TEXT("Tab opens the PDA"), [](FDescentScenarioContext& C) { PC(C)->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::Tab, IE_Pressed, 1.f)); PC(C)->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::Tab, IE_Released, 0.f)); }); S.WaitSim(0.5); S.Expect(TEXT("the PDA is open on the inventory with the flashlight and 5 flares"), [](FDescentScenarioContext& C) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(C.World); const FDescentPdaViewModel& M = Ui->Models().Pda; UE_LOG(LogDescent, Display, TEXT("M7 inventory: window %s tab %s, cells %d, cell 0 '%s' x%d, cell 1 '%s' x%d, weapon slot '%s'"), *Descent::Ui::WindowName(Ui->GetWindow()).ToString(), *Ui->GetTab().ToString(), M.Slots.Num(), M.Slots.Num() > 0 ? *M.Slots[0].Name : TEXT(""), M.Slots.Num() > 0 ? M.Slots[0].Count : 0, M.Slots.Num() > 1 ? *M.Slots[1].Name : TEXT(""), M.Slots.Num() > 1 ? M.Slots[1].Count : 0, *M.WeaponCaption); return Ui->GetWindow() == EDescentWindow::Pda && M.Slots.Num() == 24 && M.Slots[1].ItemId == TEXT("flare") && M.Slots[1].Count == 5 && M.WeaponCaption == TEXT("NOT LICENSED"); }); S.Do(TEXT("select the flares, press 4"), [](FDescentScenarioContext& C) { FDescentPdaViewModel& M = UDescentUiSubsystem::Get(C.World)->Models().Pda; if (M.OnSelect) { M.OnSelect(1); } if (M.OnAssignHotbar) { M.OnAssignHotbar(4); } }); S.WaitSim(0.4); S.Expect(TEXT("a flare is assigned to slot 4"), [](FDescentScenarioContext& C) { const UDescentCraftSubsystem* Craft = M7Craft(C); UE_LOG(LogDescent, Display, TEXT("M7 inventory: hotbar 1 '%s', 4 '%s', flares %d"), *Craft->Hotbar[0].ToString(), *Craft->Hotbar[3].ToString(), Craft->Count(TEXT("flare"))); return Craft->Hotbar[3] == TEXT("flare") && Craft->Hotbar[0] == TEXT("flashlight"); }); HudShot(S, TEXT("pda_inventory")); S.Do(TEXT("close"), [](FDescentScenarioContext& C) { UDescentUiSubsystem::Get(C.World)->Close(); }); S.WaitSim(0.3); S.Do(TEXT("press 4"), [](FDescentScenarioContext& C) { PC(C)->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::Four, IE_Pressed, 1.f)); PC(C)->InputKey(FInputKeyEventArgs::CreateSimulated(EKeys::Four, IE_Released, 0.f)); }); S.WaitSim(0.4); S.Expect(TEXT("key 4 selects the flares on the HUD hotbar"), [](FDescentScenarioContext& C) { const FDescentHudModel& H = UDescentHudSubsystem::Get(C.World)->Model(); UE_LOG(LogDescent, Display, TEXT("M7 inventory: HUD hotbar shown %d, selected %d, slot 4 '%s' x%d"), H.bShowHotbar, H.HotbarSelected, H.Hotbar.Num() > 3 ? *H.Hotbar[3].ItemId.ToString() : TEXT(""), H.Hotbar.Num() > 3 ? H.Hotbar[3].Count : 0); return H.bShowHotbar && H.HotbarSelected == 3 && H.Hotbar.Num() == 9 && H.Hotbar[3].ItemId == TEXT("flare") && H.Hotbar[3].Count == 5; }); HudShot(S, TEXT("hotbar")); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m7_save, "Save and Load: after carving, mining, placing, boxing, selling and crafting, the sections terrain.chunks_modified, ore.mined_total, build.placed, cargo.boxes, store, econ, upgrades and carrier.pos_m are the same after Load as they were at Save, although everything was changed in between.") { S.TimeLimit(420.f); DescentWorldScn::WaitStreamIdle(S, 60.f); TSharedRef M = MakeShared(); auto Carve = [](FDescentScenarioContext& C, int32 TypeFilter, const FVector& Offset) { UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(C.World); UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(C.World); if (!Ore->QueryNearest(FVector(0.0, 0.0, -30.0), TypeFilter)) { C.Fail(TEXT("no deposit")); return; } const FVector At = Ore->LastQuery.PosM + Offset; for (int32 I = 0; I < 40; ++I) { Terrain->Carve(At, 3.5, 12.0, 0.1, true); } UE_LOG(LogDescent, Display, TEXT("M7 save: carved at (%.0f, %.0f, %.0f): mined_total %lld"), At.X, At.Y, At.Z, Ore->GetMinedTotal()); }; S.Do(TEXT("carve and mine at the nearest vein"), [Carve](FDescentScenarioContext& C) { Carve(C, -1, FVector::ZeroVector); }); S.Exec(TEXT("Descent.Build.Give table 1")); S.Exec(TEXT("Descent.Build.Ghost table")); S.Exec(FString::Printf(TEXT("Descent.Build.Aim 11.0 -5.0 %.2f"), Descent::Tune::Carrier::DeckFloorZ3)); S.WaitSim(0.3); S.Exec(TEXT("Descent.Build.Place")); S.Exec(TEXT("Descent.Cargo.SpawnBox ferrite 6")); S.Exec(TEXT("Descent.Cargo.SpawnBox cobalt 4")); S.Exec(TEXT("Descent.Store.Give ingot 9")); S.Exec(TEXT("Descent.Store.Give wafer 4")); S.Exec(TEXT("Descent.Sell ingot 2")); S.Exec(TEXT("Descent.Craft depth1")); S.Exec(TEXT("Descent.Carrier.Teleport 0 0 -42 0")); S.FastForward(3.0); static const TCHAR* const Sections[] = { TEXT("store"), TEXT("econ"), TEXT("upgrades") }; auto Snapshot = [](FDescentScenarioContext& C) { TArray T; T.Add(FString::Printf(TEXT("chunks_modified %.0f"), SectionNumber(C.World, TEXT("terrain"), TEXT("chunks_modified")))); T.Add(FString::Printf(TEXT("mined_total %.0f"), SectionNumber(C.World, TEXT("ore"), TEXT("mined_total")))); T.Add(FString::Printf(TEXT("placed %.0f"), SectionNumber(C.World, TEXT("build"), TEXT("placed")))); FString Boxes; for (const ADescentOreBox* B : UDescentCargoSubsystem::Get(C.World)->Boxes()) { if (B) { Boxes += FString::Printf(TEXT("[%d x%d %s]"), B->OreType, B->Count, *B->Where); } } T.Add(TEXT("boxes ") + Boxes); for (const TCHAR* Sec : Sections) { T.Add(FString(Sec) + TEXT(" ") + SectionText(C.World, Sec)); } const FVector P = Carrier(C)->Helm.PosM; T.Add(FString::Printf(TEXT("carrier.pos_m %.2f %.2f %.2f"), P.X, P.Y, P.Z)); T.Add(FString::Printf(TEXT("tender rating %.0f"), UDescentTenderFleet::Get(C.World)->At(0)->Flight->Stats().Rating)); return T; }; S.Do(TEXT("remember the sections"), [M, Snapshot](FDescentScenarioContext& C) { M->Text = Snapshot(C); for (const FString& L : M->Text) { UE_LOG(LogDescent, Display, TEXT("M7 save: at Save: %s"), *L.Left(260)); } }); S.Exec(TEXT("Descent.Save")); // change everything S.Do(TEXT("carve and mine somewhere else"), [Carve](FDescentScenarioContext& C) { Carve(C, -1, FVector(0.0, 0.0, 45.0)); Carve(C, 1, FVector::ZeroVector); }); S.Exec(TEXT("Descent.Build.Pickup table")); S.Exec(TEXT("Descent.Build.Pickup refinery")); S.Exec(TEXT("Descent.Cargo.SpawnBox pearl 2")); S.Exec(TEXT("Descent.Sell ingot 1")); S.Exec(TEXT("Descent.Craft drill2")); S.Exec(TEXT("Descent.Carrier.Teleport 0 0 -30 0")); S.FastForward(3.0); S.Expect(TEXT("the world really changed after the save"), [M, Snapshot](FDescentScenarioContext& C) { const TArray Now = Snapshot(C); int32 Diff = 0; for (int32 I = 0; I < Now.Num(); ++I) { Diff += Now[I] != M->Text[I] ? 1 : 0; } UE_LOG(LogDescent, Display, TEXT("M7 save: %d of %d lines differ before Load"), Diff, Now.Num()); return Diff >= 6; }); S.Exec(TEXT("Descent.Load")); S.FastForward(1.0); S.Expect(TEXT("after Load every section equals what it was at Save"), [M, Snapshot](FDescentScenarioContext& C) { const TArray Now = Snapshot(C); bool bSame = Now.Num() == M->Text.Num(); for (int32 I = 0; I < Now.Num() && I < M->Text.Num(); ++I) { if (Now[I] != M->Text[I]) { bSame = false; UE_LOG(LogDescent, Display, TEXT("M7 save: DIFFERENT after Load: '%s' was '%s'"), *Now[I].Left(200), *M->Text[I].Left(200)); } } UE_LOG(LogDescent, Display, TEXT("M7 save: %d lines compared, same %d"), Now.Num(), bSame); return bSame; }); S.Expect(TEXT("the placed table and the refinery are back"), [](FDescentScenarioContext& C) { return UDescentBuildSubsystem::Get(C.World)->FindFirst(TEXT("table")) && UDescentCargoSubsystem::Get(C.World)->GetRefineries().Num() == 1; }); LocalShot(S, TEXT("after_load"), FVector(14.5, -2.6, 3.4), FVector(11.0, -5.0, 1.9), 80.f); S.DumpState(TEXT("end")); S.Quit(); }