#include "Systems/DescentSave.h" #include "Core/DescentBoot.h" #include "Debug/DescentCommandUtil.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentAutomation.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCargo.h" #include "Systems/DescentCraft.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "World/DescentOreSubsystem.h" #include "World/DescentTerrainSubsystem.h" #include "TheDescent.h" #include "Engine/World.h" #include "HAL/IConsoleManager.h" #include "Kismet/GameplayStatics.h" UDescentSaveSubsystem* UDescentSaveSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } bool UDescentSaveSubsystem::HasSave() const { return UGameplayStatics::DoesSaveGameExist(SlotName(), 0); } UDescentSaveGame* UDescentSaveSubsystem::Capture() { UWorld* World = GetWorld(); UDescentSaveGame* S = NewObject(this); const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; if (const ADescentCharacter* Ch = PC ? PC->GetDescentCharacter() : nullptr) { const FDescentVitalsState& V = Ch->Vitals->State; S->Nutrition = V.Nutrition; S->O2Max = V.O2Max; S->Health = V.Health; S->Fees = V.Debt; S->Deaths = V.Deaths; } if (const UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(World)) { for (const FDescentItemStack& St : Craft->Slots) { S->InvItems.Add(St.Item); S->InvCounts.Add(St.Count); } for (const FName& H : Craft->Hotbar) { S->Hotbar.Add(H); } S->Upgrades = Craft->Done.Array(); } const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World); if (C) { S->CarrierPosM = C->Helm.PosM; S->CarrierYawDeg = C->Helm.YawDeg; S->CarrierHull = C->Helm.Hull; S->CarrierPower = C->Helm.Power; } if (const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World)) { for (const TWeakObjectPtr& W : Fleet->All()) { FDescentSavedTender T; if (const AKazahTender* K = W.Get()) { T.bOut = K->Flight->IsOut(); T.PosUU = K->GetActorLocation(); T.Rot = K->GetActorRotation(); T.Hull = K->Flight->HullHP; T.Power = K->Flight->PowerPct; T.BioFu = K->Flight->BioFu; T.Hold = K->Flight->HoldOrder; } S->Tenders.Add(T); } } if (const UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(World)) { for (const TPair& P : Build->AllKits()) { S->KitNames.Add(P.Key); S->KitCounts.Add(P.Value); } for (const ADescentPlaceable* P : Build->Placed()) { if (!P) { continue; } FDescentSavedPlaceable Q; Q.Id = P->PlaceId; Q.Kit = P->KitId; Q.PosM = P->Place.PosM; Q.YawDeg = P->Place.YawDeg; Q.Surface = P->Place.Surface; if (const ADescentRefinery* R = Cast(P)) { Q.Queue = R->Queue; Q.Progress = R->Progress; } if (const ADescentRobotArm* A = Cast(P)) { Q.bPower = A->bPower; Q.Moved = A->Moved; } S->Placeables.Add(Q); } } if (const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World)) { S->Earned = Cargo->Earned; S->Sold = Cargo->Sold; S->Refined = { Cargo->Refined[0], Cargo->Refined[1], Cargo->Refined[2] }; S->PackerBuffer = Cargo->Packer.Buffer; S->PackerPartial = { Cargo->Packer.Partial[0], Cargo->Packer.Partial[1], Cargo->Packer.Partial[2] }; for (const ADescentOreBox* B : Cargo->Boxes()) { if (!B) { continue; } FDescentSavedBox Q; Q.Type = B->OreType; Q.Count = B->Count; Q.Where = B->Where; Q.LocalM = C ? C->ToLocalM(B->GetActorLocation()) : FVector::ZeroVector; S->Boxes.Add(Q); } } if (UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(World)) { Terrain->SaveModified(S->Terrain, S->TerrainRawSize); } if (const UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(World)) { Ore->GetState(S->OreMined, S->OreRemoved); } OnCapture.Broadcast(S); return S; } bool UDescentSaveSubsystem::Apply(const UDescentSaveGame* S) { UWorld* World = GetWorld(); if (!S || !World) { return false; } ADescentPlayerController* PC = Cast(World->GetFirstPlayerController()); ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World); UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(World); UDescentCraftSubsystem* Craft = UDescentCraftSubsystem::Get(World); if (C) { C->TeleportCarrier(S->CarrierPosM, S->CarrierYawDeg); C->Helm.Hull = S->CarrierHull; C->Helm.Power = S->CarrierPower; } if (ADescentCharacter* Ch = PC ? PC->GetDescentCharacter() : nullptr) { FDescentVitalsState& V = Ch->Vitals->State; V.Nutrition = S->Nutrition; V.O2Max = S->O2Max; V.O2 = S->O2Max; V.Health = S->Health; V.Debt = S->Fees; V.Deaths = S->Deaths; } UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World); if (Fleet) { for (int32 I = 0; I < Fleet->All().Num() && I < S->Tenders.Num(); ++I) { AKazahTender* K = Fleet->All()[I].Get(); if (!K) { continue; } const FDescentSavedTender& T = S->Tenders[I]; K->Flight->Upgrades = Descent::Tender::FTenderUpgrades(); K->Flight->HoldOrder = T.Hold; K->Flight->BioFu = T.BioFu; K->Flight->PowerPct = T.Power; K->Flight->HullHP = T.Hull; // an undocked Tender is restored where it was left (one that is docked now stays in its cradle) if (T.bOut && K->Flight->State == ETenderState::Abandoned) { K->SetActorLocationAndRotation(T.PosUU, T.Rot); K->Flight->SyncFromActor(); } } } if (Craft) { Craft->ResetAll(); for (int32 I = 0; I < Craft->Slots.Num(); ++I) { Craft->Slots[I].Item = S->InvItems.IsValidIndex(I) ? S->InvItems[I] : NAME_None; Craft->Slots[I].Count = S->InvCounts.IsValidIndex(I) ? S->InvCounts[I] : 0; } for (int32 I = 0; I < 9; ++I) { Craft->Hotbar[I] = S->Hotbar.IsValidIndex(I) ? S->Hotbar[I] : NAME_None; } for (const FName& U : S->Upgrades) { Craft->Done.Add(U); Craft->ApplyOne(U, true); } } TMap NewId; if (Cargo) { Cargo->DestroyAllBoxes(); } if (Build) { Build->CancelGhost(); Build->DestroyAll(); Build->ResetKits(); for (int32 I = 0; I < S->KitNames.Num() && I < S->KitCounts.Num(); ++I) { Build->AddKit(S->KitNames[I], S->KitCounts[I]); } for (const FDescentSavedPlaceable& Q : S->Placeables) { ADescentPlaceable* P = Build->Spawn(Q.Kit, Q.PosM, Q.YawDeg, Q.Surface); if (!P) { continue; } NewId.Add(Q.Id, P->PlaceId); if (ADescentRefinery* R = Cast(P)) { R->Queue = Q.Queue; R->Progress = Q.Progress; } if (ADescentRobotArm* A = Cast(P)) { A->bPower = Q.bPower; A->Moved = Q.Moved; } } } if (Cargo) { Cargo->Earned = S->Earned; Cargo->Sold = S->Sold; for (int32 I = 0; I < 3; ++I) { Cargo->Refined[I] = S->Refined.IsValidIndex(I) ? S->Refined[I] : 0; Cargo->Packer.Partial[I] = S->PackerPartial.IsValidIndex(I) ? S->PackerPartial[I] : 0; } Cargo->Packer.Buffer = S->PackerBuffer; for (const FDescentSavedBox& Q : S->Boxes) { FString Where = Q.Where; FString Id, Slot; if (Where.StartsWith(TEXT("rack")) && Where.RightChop(4).Split(TEXT("."), &Id, &Slot)) { const int32* N = NewId.Find(FCString::Atoi(*Id)); Where = FString::Printf(TEXT("rack%d.%s"), N ? *N : -1, *Slot); } else if (!Where.StartsWith(TEXT("packer.out"))) { Where = TEXT("floor"); } // a box in the hands or in an arm lands on the deck Cargo->RestoreBox(Q.Type, Q.Count, Where, Q.LocalM); } } if (UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(World)) { Terrain->LoadModified(S->Terrain, S->TerrainRawSize); } if (UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(World)) { Ore->SetState(S->OreMined, S->OreRemoved); } OnApply.Broadcast(S); return true; } bool UDescentSaveSubsystem::SaveToSlot(FString* OutWhyNot) { UDescentSaveGame* S = Capture(); if (!S || !UGameplayStatics::SaveGameToSlot(S, SlotName(), 0)) { if (OutWhyNot) { *OutWhyNot = TEXT("could not write the save slot"); } return false; } ++Saves; if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(TEXT("Saved"), EDescentHudToastKind::Good); } return true; } bool UDescentSaveSubsystem::LoadFromSlot(FString* OutWhyNot) { const UDescentSaveGame* S = Cast(UGameplayStatics::LoadGameFromSlot(SlotName(), 0)); if (!S) { if (OutWhyNot) { *OutWhyNot = TEXT("no save in slot descent_0"); } return false; } if (!Apply(S)) { if (OutWhyNot) { *OutWhyNot = TEXT("could not apply the save"); } return false; } ++Loads; return true; } // ---- the title card's CONTINUE and the pause window's SAVE --------------------------------------------------------------- DESCENT_BOOT_STEP(WireSave, Descent::Boot::Order::LoadSave) { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(Ctx.World); UDescentSaveSubsystem* Save = UDescentSaveSubsystem::Get(Ctx.World); if (!Ui || !Save) { return; } TWeakObjectPtr Weak(Save); TWeakObjectPtr WeakUi(Ui); Ui->Models().Pause.OnSave = [Weak]() { if (UDescentSaveSubsystem* S = Weak.Get()) { S->SaveToSlot(); } }; Ui->Models().Title.bCanContinue = Save->HasSave(); Ui->Models().Title.OnContinue = [Weak, WeakUi]() { UDescentSaveSubsystem* S = Weak.Get(); if (!S) { return; } S->LoadFromSlot(); if (UDescentUiSubsystem* U = WeakUi.Get()) { U->Close(); } if (ADescentPlayerController* P = Cast(S->GetWorld()->GetFirstPlayerController())) { if (P->GetMode() == EDescentMode::Title) { P->SetMode(EDescentMode::Foot); } } }; Ui->NotifyChanged(); } namespace { void CmdSave(const TArray&, UWorld* World) { UDescentSaveSubsystem* S = UDescentSaveSubsystem::Get(World); FString Why = TEXT("no save subsystem"); if (!S || !S->SaveToSlot(&Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Save"), *Why); } } void CmdLoad(const TArray&, UWorld* World) { UDescentSaveSubsystem* S = UDescentSaveSubsystem::Get(World); FString Why = TEXT("no save subsystem"); if (!S || !S->LoadFromSlot(&Why)) { Descent::Cmd::Fail(World, TEXT("Descent.Load"), *Why); } } FAutoConsoleCommandWithWorldAndArgs GCmdSave(TEXT("Descent.Save"), TEXT("Descent.Save - write slot descent_0"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdSave)); FAutoConsoleCommandWithWorldAndArgs GCmdLoad(TEXT("Descent.Load"), TEXT("Descent.Load - read slot descent_0 into the running world"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&CmdLoad)); } static FDescentStateProviderRegistrar GSaveStateSection(TEXT("save"), [](UWorld* World, FJsonObject& Out) { const UDescentSaveSubsystem* S = UDescentSaveSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } Out.SetBoolField(TEXT("has_save"), S->HasSave()); Out.SetNumberField(TEXT("saves"), S->Saves); Out.SetNumberField(TEXT("loads"), S->Loads); });