#include "Systems/DescentCraft.h" #include "Audio/DescentAudio.h" #include "Core/DescentSimSubsystem.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Player/DescentVitals.h" #include "Systems/DescentBuild.h" #include "Systems/DescentCargo.h" #include "UI/DescentHudSubsystem.h" #include "UI/DescentUiSubsystem.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "TheDescent.h" #include "Engine/World.h" namespace Descent::Craft { FString ResName(int32 Res) { static const TCHAR* const Names[NumRes] = { TEXT("Ingot"), TEXT("Wafer"), TEXT("Lattice"), TEXT("Ferrite"), TEXT("Cobalt") }; return Res >= 0 && Res < NumRes ? Names[Res] : TEXT("?"); } const TArray& Recipes() { static TArray Table; if (Table.Num() > 0) { return Table; } auto Add = [](const TCHAR* Id, const TCHAR* Cat, const TCHAR* Name, const TCHAR* Desc, int32 I, int32 W, int32 L, int32 F, int32 C) -> FRecipe& { FRecipe R; R.Id = Id; R.Cat = Cat; R.Name = Name; R.Desc = Desc; R.Cost[Ingot] = I; R.Cost[Wafer] = W; R.Cost[Lattice] = L; R.Cost[Ferrite] = F; R.Cost[Cobalt] = C; return Table.Add_GetRef(R); }; Add(TEXT("hull_reinforce"), TEXT("submersible"), TEXT("Hull Reinforcement"), TEXT("Tender hull x1.5"), 3, 0, 0, 0, 0); Add(TEXT("depth1"), TEXT("submersible"), TEXT("Depth Module Mk I"), TEXT("Rating 2,300 to 2,700 m"), 3, 2, 0, 0, 0); Add(TEXT("depth2"), TEXT("submersible"), TEXT("Depth Module Mk II"), TEXT("Rating to 3,150 m"), 0, 3, 2, 0, 0).Needs = TEXT("depth1"); Add(TEXT("power2"), TEXT("submersible"), TEXT("Power Cell II"), TEXT("Capacity x1.7"), 2, 2, 0, 0, 0); Add(TEXT("thruster"), TEXT("submersible"), TEXT("Thruster Upgrade"), TEXT("Speed +25%"), 3, 0, 0, 0, 1); Add(TEXT("sonar_module"), TEXT("submersible"), TEXT("Sonar Module"), TEXT("Ping 260 m; flags honest voids red"), 0, 2, 0, 0, 0); Add(TEXT("drill2"), TEXT("mining"), TEXT("Drill Head Mk II"), TEXT("Radius +0.7 m, rate +50%"), 3, 0, 0, 0, 0); Add(TEXT("drill3"), TEXT("mining"), TEXT("Drill Head Mk III"), TEXT("Radius and rate again"), 2, 2, 1, 0, 0).Needs = TEXT("drill2"); Add(TEXT("laser"), TEXT("mining"), TEXT("Laser Cutter"), TEXT("Alternative tool head (T)"), 0, 2, 1, 0, 0); Add(TEXT("suction2"), TEXT("mining"), TEXT("Suction Mk II"), TEXT("Range 22 m, pull 16"), 2, 0, 0, 0, 1); Add(TEXT("hold"), TEXT("mining"), TEXT("Hold Expansion"), TEXT("Every hold +8"), 3, 0, 0, 0, 0); Add(TEXT("hull_patch"), TEXT("carrier"), TEXT("Carrier Hull Patch"), TEXT("Carrier hull +25"), 4, 1, 0, 0, 0).bRepeatable = true; { FRecipe& R = Add(TEXT("hand_sonar"), TEXT("explore"), TEXT("Handheld Sonar"), TEXT("Ping 90 m while swimming"), 1, 1, 0, 0, 0); R.Item = TEXT("hand_sonar"); R.ItemCount = 1; } { FRecipe& R = Add(TEXT("flares"), TEXT("explore"), TEXT("Flares x5"), TEXT("Thrown lights, 120 s each"), 0, 0, 0, 2, 0); R.Item = TEXT("flare"); R.ItemCount = 5; R.bRepeatable = true; } { FRecipe& R = Add(TEXT("beacons"), TEXT("explore"), TEXT("Marker Beacons x2"), TEXT("A marker on the map and the HUD"), 1, 0, 0, 0, 1); R.Item = TEXT("beacon"); R.ItemCount = 2; R.bRepeatable = true; } Add(TEXT("o2_high"), TEXT("explore"), TEXT("High-Capacity O2 Tank"), TEXT("90 s of air"), 2, 0, 0, 0, 0); Add(TEXT("o2_ultra"), TEXT("explore"), TEXT("Ultra O2 Tank"), TEXT("150 s of air"), 1, 1, 1, 0, 0); { FRecipe& R = Add(TEXT("propulsor"), TEXT("explore"), TEXT("Sea-Glide Propulsor"), TEXT("Hold LMB: 7.5 m/s"), 2, 1, 0, 0, 0); R.Item = TEXT("propulsor"); R.ItemCount = 1; } auto Kit = [&Add](const TCHAR* Id, const TCHAR* Name, const TCHAR* Desc, int32 I, int32 W, int32 L) -> FRecipe& { FRecipe& R = Add(*FString::Printf(TEXT("kit_%s"), Id), TEXT("build"), Name, Desc, I, W, L, 0, 0); R.Kit = Id; R.bRepeatable = true; return R; }; Kit(TEXT("locker"), TEXT("Storage Locker"), TEXT("+40 storage while placed"), 4, 0, 0); Kit(TEXT("rack"), TEXT("Box Rack"), TEXT("4 box slots"), 2, 0, 0); Kit(TEXT("arm1"), TEXT("Handling Arm Mk I"), TEXT("Reach 3.2 m, a box every 4.0 s"), 4, 2, 0); Kit(TEXT("arm2"), TEXT("Handling Arm Mk II"), TEXT("Reach 4.5 m, a box every 2.5 s"), 2, 2, 1).Needs = TEXT("kit_arm1"); Kit(TEXT("light"), TEXT("Work Light"), TEXT("Wall or ceiling lamp"), 1, 0, 0); Kit(TEXT("table"), TEXT("Table"), TEXT("Furniture"), 1, 0, 0); Kit(TEXT("chair"), TEXT("Chair"), TEXT("Furniture"), 1, 0, 0); Kit(TEXT("shelf"), TEXT("Shelf"), TEXT("Furniture"), 1, 0, 0); Kit(TEXT("bunk"), TEXT("Bunk"), TEXT("Sleep anywhere"), 2, 0, 0); Kit(TEXT("couch"), TEXT("Couch"), TEXT("Furniture"), 2, 0, 0); Kit(TEXT("fabricator"), TEXT("Fabricator"), TEXT("A second fabricator"), 6, 2, 0); Kit(TEXT("refinery"), TEXT("Refinery"), TEXT("A second refinery"), 8, 3, 0); Add(TEXT("harpoon"), TEXT("weapons"), TEXT("Harpoon Sidearm"), TEXT("Tethys has not licensed weapons"), 0, 0, 0, 0, 0).bLocked = true; return Table; } const FRecipe* Find(FName Id) { for (const FRecipe& R : Recipes()) { if (R.Id == Id) { return &R; } } return nullptr; } bool Pay(const FRecipe& R, int32 Have[NumRes]) { for (int32 I = 0; I < NumRes; ++I) { if (Have[I] < R.Cost[I]) { return false; } } for (int32 I = 0; I < NumRes; ++I) { Have[I] -= R.Cost[I]; } return true; } FString ItemName(FName Item) { if (Item == TEXT("flashlight")) { return TEXT("Flashlight"); } if (Item == TEXT("flare")) { return TEXT("Flare"); } if (Item == TEXT("beacon")) { return TEXT("Marker Beacon"); } if (Item == TEXT("hand_sonar")) { return TEXT("Handheld Sonar"); } if (Item == TEXT("propulsor")) { return TEXT("Sea-Glide Propulsor"); } return Item.IsNone() ? FString() : Item.ToString(); } } namespace DC = Descent::Craft; UDescentCraftSubsystem* UDescentCraftSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentCraftSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); ResetAll(); if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Craft"); Opt.SubOrder = 30; StepHandle = Sim->RegisterStep(EDescentSimStep::Cargo, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentCraftSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } void UDescentCraftSubsystem::ResetAll() { Done.Reset(); Slots.Reset(); Slots.SetNum(24); for (FName& H : Hotbar) { H = NAME_None; } HotbarSelected = 0; PdaSelected = -1; // the suit starts with the flashlight in slot 1 AddItem(TEXT("flashlight"), 1); Hotbar[0] = TEXT("flashlight"); } int32 UDescentCraftSubsystem::Count(FName Item) const { int32 N = 0; for (const FDescentItemStack& S : Slots) { if (S.Item == Item) { N += S.Count; } } return N; } bool UDescentCraftSubsystem::AddItem(FName Item, int32 N) { if (Item.IsNone() || N <= 0) { return false; } for (FDescentItemStack& S : Slots) { if (S.Item == Item) { S.Count += N; return true; } } for (FDescentItemStack& S : Slots) { if (S.Item.IsNone()) { S.Item = Item; S.Count = N; return true; } } return false; } bool UDescentCraftSubsystem::RemoveItem(FName Item, int32 N) { for (FDescentItemStack& S : Slots) { if (S.Item == Item && S.Count >= N) { S.Count -= N; if (S.Count == 0) { S.Item = NAME_None; for (FName& H : Hotbar) { if (H == Item) { H = NAME_None; } } } return true; } } return false; } bool UDescentCraftSubsystem::AssignHotbar(int32 Slot1to9, FName Item) { if (Slot1to9 < 1 || Slot1to9 > 9 || (!Item.IsNone() && Count(Item) <= 0)) { return false; } for (FName& H : Hotbar) { if (H == Item) { H = NAME_None; } } // an item sits in one slot only Hotbar[Slot1to9 - 1] = Item; return true; } void UDescentCraftSubsystem::GatherHave(int32 Have[DC::NumRes]) const { const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); for (int32 I = 0; I < DC::NumRes; ++I) { Have[I] = 0; } if (!Cargo) { return; } for (int32 I = 0; I < 3; ++I) { Have[I] = Cargo->Refined[I]; } Have[DC::Ferrite] = Cargo->CountOre(0); Have[DC::Cobalt] = Cargo->CountOre(1); } bool UDescentCraftSubsystem::CanCraft(FName Id, FString* OutWhyNot) const { auto No = [OutWhyNot](const TCHAR* Why) { if (OutWhyNot) { *OutWhyNot = Why; } return false; }; const DC::FRecipe* R = DC::Find(Id); if (!R) { return No(TEXT("unknown recipe")); } if (R->bLocked) { return No(TEXT("Tethys has not licensed weapons")); } if (IsBuilding()) { return No(TEXT("The fabricator is building")); } if (!R->bRepeatable && Done.Contains(Id)) { return No(TEXT("Already installed")); } if (!R->Needs.IsNone() && !Done.Contains(R->Needs)) { const DC::FRecipe* N = DC::Find(R->Needs); return No(*FString::Printf(TEXT("Needs %s"), N ? *N->Name : TEXT("another module"))); } if (Id == TEXT("hull_patch")) { const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (!C || C->Helm.Hull >= 99.5) { return No(TEXT("The hull is not damaged")); } } int32 Have[DC::NumRes]; GatherHave(Have); if (!DC::Pay(*R, Have)) { return No(TEXT("Not enough material")); } return true; } bool UDescentCraftSubsystem::Craft(FName Id, FString* OutWhyNot) { if (!CanCraft(Id, OutWhyNot)) { return false; } const DC::FRecipe* R = DC::Find(Id); UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); // the cost comes from storage and from the ore boxes for (int32 I = 0; I < 3; ++I) { Cargo->Refined[I] -= R->Cost[I]; } if (R->Cost[DC::Ferrite] > 0) { Cargo->ConsumeOre(0, R->Cost[DC::Ferrite]); } if (R->Cost[DC::Cobalt] > 0) { Cargo->ConsumeOre(1, R->Cost[DC::Cobalt]); } Done.Add(Id); ++Crafted; BuildTimer = 2.0; ApplyOne(Id); if (UDescentAudioSubsystem* Audio = UDescentAudioSubsystem::Get(GetWorld())) { Audio->OneShot(TEXT("craft")); } if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(GetWorld())) { Hud->Model().PushToast(FString::Printf(TEXT("Built: %s"), *R->Name), EDescentHudToastKind::Good); } FillWindows(); return true; } void UDescentCraftSubsystem::ApplyOne(FName Id, bool bFromLoad) { const DC::FRecipe* R = DC::Find(Id); UWorld* World = GetWorld(); if (!R || !World) { return; } if (!bFromLoad) { if (!R->Kit.IsNone()) { if (UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(World)) { Build->AddKit(R->Kit, 1); } } if (!R->Item.IsNone()) { AddItem(R->Item, R->ItemCount); } if (Id == TEXT("hull_patch")) { if (ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World)) { C->Helm.Hull = FMath::Min(100.0, C->Helm.Hull + 25.0); } } } if (UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World)) { for (const TWeakObjectPtr& W : Fleet->All()) { AKazahTender* T = W.Get(); if (!T) { continue; } Descent::Tender::FTenderUpgrades& U = T->Flight->Upgrades; if (Id == TEXT("hull_reinforce")) { const double Frac = T->Flight->HullHP / T->Flight->Stats().HullMax; U.HullMk = 1; T->Flight->HullHP = Frac * T->Flight->Stats().HullMax; } else if (Id == TEXT("depth1")) { U.DepthMk = FMath::Max(U.DepthMk, 1); } else if (Id == TEXT("depth2")) { U.DepthMk = 2; } else if (Id == TEXT("power2")) { U.bPowerCell2 = true; } else if (Id == TEXT("thruster")) { U.ThrustMk = 1; } else if (Id == TEXT("sonar_module")) { U.bSonarModule = true; } else if (Id == TEXT("drill2")) { U.DrillMk = FMath::Max(U.DrillMk, 1); } else if (Id == TEXT("drill3")) { U.DrillMk = 2; } else if (Id == TEXT("laser")) { U.bLaser = true; } else if (Id == TEXT("suction2")) { U.SuctionMk = 1; } else if (Id == TEXT("hold")) { U.HoldMk = 1; } } } if (Id == TEXT("o2_high") || Id == TEXT("o2_ultra")) { const APlayerController* PC = World->GetFirstPlayerController(); if (ADescentCharacter* Ch = PC ? Cast(PC->GetPawn()) : nullptr) { const double Max = Done.Contains(TEXT("o2_ultra")) ? 150.0 : 90.0; Ch->Vitals->State.O2Max = FMath::Max(Ch->Vitals->State.O2Max, Max); Ch->Vitals->State.O2 = Ch->Vitals->State.O2Max; } } } void UDescentCraftSubsystem::Step(double Dt) { if (BuildTimer > 0.0) { BuildTimer = FMath::Max(0.0, BuildTimer - Dt); } WindowTimer -= Dt; if (WindowTimer <= 0.0) { WindowTimer = 0.25; FillWindows(); } // the hotbar of the HUD: on foot and swimming UWorld* World = GetWorld(); UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(World); const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; if (Hud && PC) { FDescentHudModel& M = Hud->Model(); M.bShowHotbar = PC->GetMode() == EDescentMode::Foot || PC->GetMode() == EDescentMode::Swim; // M10: the objective line says the next step of the loop const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(World); const AKazahTender* T = PC->GetTender(); switch (PC->GetMode()) { case EDescentMode::Sub: M.Objective = (T && T->Flight->HoldOrder.Num() > 0) ? TEXT("Dock: fly up under the moon pool, press E") : TEXT("Mine: LMB drill, RMB suction. R sonar"); break; case EDescentMode::Docked: M.Objective = TEXT("SPACE launch. E leave the Tender"); break; case EDescentMode::Helm: M.Objective = TEXT("Helm: mouse aims, W S lever. E leave"); break; case EDescentMode::Swim: M.Objective = TEXT("E at the pool ladder climbs out"); break; case EDescentMode::Foot: if (Cargo && Cargo->Carried()) { M.Objective = TEXT("Load the box into the refinery hopper (E)"); } else if (Cargo && Cargo->Boxes().Num() > 0) { M.Objective = TEXT("Carry the ore box from the packer to the refinery (E)"); } else if (Cargo && Cargo->StorageUsed() > 0) { M.Objective = TEXT("Refined goods: fabricator builds, terminal sells"); } else { M.Objective = TEXT("Sub bay: board a Tender (E)"); } break; default: M.Objective.Reset(); break; } M.HotbarSelected = HotbarSelected; M.Hotbar.SetNum(9); for (int32 I = 0; I < 9; ++I) { M.Hotbar[I].ItemId = Hotbar[I]; M.Hotbar[I].Label = DC::ItemName(Hotbar[I]).Left(2).ToUpper(); M.Hotbar[I].Count = Hotbar[I].IsNone() ? 0 : Count(Hotbar[I]); } } } void UDescentCraftSubsystem::OpenFabricator() { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); if (!Ui) { return; } Ui->Open(EDescentWindow::Fabricator); FillWindows(); } void UDescentCraftSubsystem::FillWindows() { UDescentUiSubsystem* Ui = UDescentUiSubsystem::Get(GetWorld()); if (!Ui || !Ui->IsWindowOpen() || Ui->IsDemo()) { return; } // the demo owns the models while it runs const EDescentWindow W = Ui->GetWindow(); TWeakObjectPtr Weak(this); const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld()); const UDescentBuildSubsystem* Build = UDescentBuildSubsystem::Get(GetWorld()); if (W == EDescentWindow::Fabricator) { int32 Have[DC::NumRes]; GatherHave(Have); FDescentFabricatorViewModel& M = Ui->Models().Fabricator; M.Categories.Reset(); for (const DC::FRecipe& R : DC::Recipes()) { FDescentFabricatorCategory* Cat = M.Categories.FindByPredicate([&R](const FDescentFabricatorCategory& C) { return C.Id == R.Cat; }); if (!Cat) { Cat = &M.Categories.AddDefaulted_GetRef(); Cat->Id = R.Cat; Cat->Label = R.Cat == TEXT("explore") ? FString(TEXT("EXPLORATION")) : R.Cat.ToString().ToUpper(); } FDescentRecipeRow Row; Row.Id = R.Id; Row.IconItem = R.Id; Row.Name = R.Name; Row.Desc = R.Desc; for (int32 I = 0; I < DC::NumRes; ++I) { if (R.Cost[I] > 0) { FDescentUiCost C; C.ItemId = FName(*DC::ResName(I).ToLower()); C.Name = DC::ResName(I); C.Need = R.Cost[I]; C.Have = Have[I]; Row.Costs.Add(C); } } FString Why; const bool bCan = CanCraft(R.Id, &Why); const bool bInstalled = !R.bRepeatable && Done.Contains(R.Id); Row.bLocked = R.bLocked || bInstalled || (!R.Needs.IsNone() && !Done.Contains(R.Needs)) || (R.Id == TEXT("hull_patch") && !bCan && Why.Contains(TEXT("damaged"))); Row.LockedReason = Row.bLocked ? (bInstalled ? FString(TEXT("Installed")) : Why) : FString(); Row.ButtonText = bInstalled ? TEXT("DONE") : TEXT("BUILD"); const FName Id = R.Id; Row.OnBuild = [Weak, Id]() { UDescentCraftSubsystem* S = Weak.Get(); FString Reason; if (S && !S->Craft(Id, &Reason)) { if (UDescentHudSubsystem* Hud = UDescentHudSubsystem::Get(S->GetWorld())) { Hud->Model().PushToast(Reason, EDescentHudToastKind::Warn); } } }; Cat->Rows.Add(MoveTemp(Row)); } M.BuildProgress = IsBuilding() ? static_cast(1.0 - BuildTimer / 2.0) : 0.f; M.StatusText = IsBuilding() ? TEXT("Building") : TEXT("Ready"); Ui->NotifyChanged(); } else if (W == EDescentWindow::Pda) { FDescentPdaViewModel& M = Ui->Models().Pda; M.Slots.SetNum(24); for (int32 I = 0; I < 24; ++I) { M.Slots[I].ItemId = Slots[I].Item; M.Slots[I].Name = DC::ItemName(Slots[I].Item); M.Slots[I].Count = Slots[I].Count; } M.Selected = PdaSelected; const bool bSel = Slots.IsValidIndex(PdaSelected) && !Slots[PdaSelected].Item.IsNone(); M.SelectedName = bSel ? DC::ItemName(Slots[PdaSelected].Item) : FString(); M.SelectedDesc = bSel ? FString(TEXT("Press 1 to 9 to put it on the hotbar.")) : FString(); M.O2Tank.ItemId = Done.Contains(TEXT("o2_ultra")) ? FName(TEXT("o2_ultra")) : (Done.Contains(TEXT("o2_high")) ? FName(TEXT("o2_high")) : FName(TEXT("o2_base"))); M.O2Tank.Name = Done.Contains(TEXT("o2_ultra")) ? TEXT("Ultra O2 Tank") : (Done.Contains(TEXT("o2_high")) ? TEXT("High-Capacity O2 Tank") : TEXT("Standard O2 Tank")); M.O2Tank.Count = 1; M.OnSelect = [Weak](int32 I) { if (UDescentCraftSubsystem* S = Weak.Get()) { S->PdaSelected = I; S->FillWindows(); } }; M.OnAssignHotbar = [Weak](int32 Slot) { UDescentCraftSubsystem* S = Weak.Get(); if (S && S->Slots.IsValidIndex(S->PdaSelected) && !S->Slots[S->PdaSelected].Item.IsNone()) { S->AssignHotbar(Slot, S->Slots[S->PdaSelected].Item); } }; Ui->NotifyChanged(); } else if (W == EDescentWindow::Storage && Cargo) { static const TCHAR* const Names[3] = { TEXT("Ferrite Ingot"), TEXT("Cobalt Wafer"), TEXT("Pearl Lattice") }; FDescentStorageViewModel& M = Ui->Models().Storage; M.Goods.Reset(); M.Kits.Reset(); for (int32 T = 0; T < 3; ++T) { M.Goods.Add({ Descent::Cargo::RefinedName(static_cast(T)), Names[T], Cargo->Refined[T] }); } if (Build) { for (const Descent::Build::FKitDef& K : Descent::Build::Kits()) { if (Build->KitCount(K.Id) > 0) { M.Kits.Add({ K.Id, K.Name, Build->KitCount(K.Id) }); } } } M.Used = Cargo->StorageUsed(); M.Cap = Cargo->StorageCap; Ui->NotifyChanged(); } } // ---- state sections `inventory`, `upgrades` (section 9.4) --------------------------------------------------------------- static FDescentStateProviderRegistrar GInventoryStateSection(TEXT("inventory"), [](UWorld* World, FJsonObject& Out) { const UDescentCraftSubsystem* S = UDescentCraftSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } TArray> Slots, Bar; for (const FDescentItemStack& St : S->Slots) { TSharedRef J = MakeShared(); J->SetStringField(TEXT("item"), St.Item.IsNone() ? FString() : St.Item.ToString()); J->SetNumberField(TEXT("count"), St.Count); Slots.Add(MakeShared(J)); } for (const FName& H : S->Hotbar) { Bar.Add(MakeShared(H.IsNone() ? FString() : H.ToString())); } Out.SetArrayField(TEXT("slots"), Slots); Out.SetArrayField(TEXT("hotbar"), Bar); Out.SetNumberField(TEXT("hotbar_selected"), S->HotbarSelected + 1); Out.SetNumberField(TEXT("flares"), S->Count(TEXT("flare"))); Out.SetStringField(TEXT("weapon_slot"), TEXT("NOT LICENSED")); }); static FDescentStateProviderRegistrar GUpgradesStateSection(TEXT("upgrades"), [](UWorld* World, FJsonObject& Out) { const UDescentCraftSubsystem* S = UDescentCraftSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } TArray Names = S->Done.Array(); Names.Sort(FNameLexicalLess()); TArray> List; for (const FName& N : Names) { List.Add(MakeShared(N.ToString())); } Out.SetArrayField(TEXT("done"), List); Out.SetNumberField(TEXT("count"), Names.Num()); Out.SetBoolField(TEXT("building"), S->IsBuilding()); });