#include "World/DescentWorldDef.h" #include "Core/Tuning/DescentTune_World.h" #include "Core/Tuning/DescentTune_Terrain.h" #include "HAL/PlatformTime.h" namespace WTune = Descent::Tune::World; // ------------------------------------------------------------------------------------------------------------ // File-local helpers (prefixed WorldDef so a unity build never meets another file's `Fnv`, `Turtle`, ...) // ------------------------------------------------------------------------------------------------------------ namespace { double GWorldDefBuildMs = 0.0; /** FNV-1a, 64 bit. */ struct FWorldDefFnv { uint64 H = 0xcbf29ce484222325ull; void Bytes(const void* Data, int32 Num) { const uint8* B = static_cast(Data); for (int32 I = 0; I < Num; ++I) { H ^= B[I]; H *= 0x100000001b3ull; } } void Num(double V) { // Round to a micrometre first so -0.0 and platform noise in the last bit cannot change the hash. const int64 Q = static_cast(FMath::RoundToDouble(V * 1e6)); Bytes(&Q, sizeof(Q)); } void Int(int64 V) { Bytes(&V, sizeof(V)); } void Vec(const FVector& V) { Num(V.X); Num(V.Y); Num(V.Z); } void Str(const FString& S) { Bytes(*S, S.Len() * sizeof(TCHAR)); } }; /** Turtle-graphics gallery builder (port of 03_world.js turtlePath, in Unreal axes: psi 0 = +X, positive toward +Y). */ struct FWorldDefTurtleStep { TCHAR Kind; // 's' straight, 't' turn double A; // length (s) or turn angle in degrees (t) double R; // turn radius (t) }; void WorldDefTurtlePath(const FVector& Start, double YawDeg, double PitchDeg, double Radius, const TArray& Steps, TArray& Out) { double X = Start.X, Y = Start.Y, Z = Start.Z; double Yaw = FMath::DegreesToRadians(YawDeg); const double Pitch = FMath::DegreesToRadians(PitchDeg); Out.Add(FVector4(X, Y, Z, Radius)); auto Move = [&](double Ds) { X += FMath::Cos(Yaw) * FMath::Cos(Pitch) * Ds; Y += FMath::Sin(Yaw) * FMath::Cos(Pitch) * Ds; Z += FMath::Sin(Pitch) * Ds; Out.Add(FVector4(X, Y, Z, Radius)); }; for (const FWorldDefTurtleStep& St : Steps) { if (St.Kind == TEXT('s')) { const int32 N = FMath::Max(1, FMath::RoundToInt(St.A / WTune::TurtleStepM)); for (int32 I = 0; I < N; ++I) { Move(St.A / N); } } else { const double Ang = FMath::DegreesToRadians(St.A); const double Arc = FMath::Abs(Ang) * St.R / FMath::Cos(Pitch); const int32 N = FMath::Max(2, FMath::RoundToInt(Arc / WTune::TurtleStepM)); for (int32 I = 0; I < N; ++I) { Yaw += Ang / N / 2.0; Move(Arc / N); Yaw += Ang / N / 2.0; } } } } /** Centripetal Catmull-Rom through Pts at T in [0,1] (three.js CatmullRomCurve3 'centripetal', open). */ FVector WorldDefCatmullRom(const TArray& Pts, double T) { const int32 L = Pts.Num(); const double Pp = (L - 1) * T; int32 I = FMath::FloorToInt(Pp); double W = Pp - I; if (W == 0.0 && I == L - 1) { I = L - 2; W = 1.0; } const FVector P1 = Pts[I], P2 = Pts[I + 1]; const FVector P0 = I > 0 ? Pts[I - 1] : P1 + (P1 - P2); const FVector P3 = (I + 2 < L) ? Pts[I + 2] : P2 + (P2 - P1); double Dt0 = FMath::Pow(FVector::DistSquared(P0, P1), 0.25); double Dt1 = FMath::Pow(FVector::DistSquared(P1, P2), 0.25); double Dt2 = FMath::Pow(FVector::DistSquared(P2, P3), 0.25); if (Dt1 < 1e-4) { Dt1 = 1.0; } if (Dt0 < 1e-4) { Dt0 = Dt1; } if (Dt2 < 1e-4) { Dt2 = Dt1; } auto Axis = [&](double X0, double X1, double X2, double X3) { double T1 = (X1 - X0) / Dt0 - (X2 - X0) / (Dt0 + Dt1) + (X2 - X1) / Dt1; double T2 = (X2 - X1) / Dt1 - (X3 - X1) / (Dt1 + Dt2) + (X3 - X2) / Dt2; T1 *= Dt1; T2 *= Dt1; const double C2 = -3.0 * X1 + 3.0 * X2 - 2.0 * T1 - T2; const double C3 = 2.0 * X1 - 2.0 * X2 + T1 + T2; return X1 + T1 * W + C2 * W * W + C3 * W * W * W; }; return FVector(Axis(P0.X, P1.X, P2.X, P3.X), Axis(P0.Y, P1.Y, P2.Y, P3.Y), Axis(P0.Z, P1.Z, P2.Z, P3.Z)); } /** Spatial hash cell of a point. */ FIntVector WorldDefCell(double X, double Y, double Z) { return FIntVector(FMath::FloorToInt(X / WTune::BucketCell), FMath::FloorToInt(Y / WTune::BucketCell), FMath::FloorToInt(Z / WTune::BucketCell)); } void WorldDefInsertBox(TMap>& Map, const FVector& Lo, const FVector& Hi, int32 Index) { const FIntVector A = WorldDefCell(Lo.X, Lo.Y, Lo.Z), B = WorldDefCell(Hi.X, Hi.Y, Hi.Z); for (int32 Z = A.Z; Z <= B.Z; ++Z) { for (int32 Y = A.Y; Y <= B.Y; ++Y) { for (int32 X = A.X; X <= B.X; ++X) { Map.FindOrAdd(FIntVector(X, Y, Z)).Add(Index); } } } } void WorldDefCollect(const TMap>& Map, const FVector& C, double Rad, TArray>& Out) { Out.Reset(); const FIntVector A = WorldDefCell(C.X - Rad, C.Y - Rad, C.Z - Rad), B = WorldDefCell(C.X + Rad, C.Y + Rad, C.Z + Rad); for (int32 Z = A.Z; Z <= B.Z; ++Z) { for (int32 Y = A.Y; Y <= B.Y; ++Y) { for (int32 X = A.X; X <= B.X; ++X) { if (const TArray* L = Map.Find(FIntVector(X, Y, Z))) { Out.Append(*L); } } } } Out.Sort(); int32 W = 0; for (int32 I = 0; I < Out.Num(); ++I) { if (I == 0 || Out[I] != Out[I - 1]) { Out[W++] = Out[I]; } } Out.SetNum(W, EAllowShrinking::No); } } namespace Descent::World { FLinearColor OreGlowColor(EOreType Type) { switch (Type) { case EOreType::Ferrite: return FLinearColor(1.0f, 0.45f, 0.12f); case EOreType::Cobalt: return FLinearColor(0.15f, 0.75f, 1.0f); default: return FLinearColor(1.0f, 0.25f, 0.8f); } } const TCHAR* OreName(EOreType Type) { switch (Type) { case EOreType::Ferrite: return TEXT("ferrite"); case EOreType::Cobalt: return TEXT("cobalt"); default: return TEXT("pearl"); } } FDescentWorldDef::FDescentWorldDef() : SdfObj(*this) { } const FDescentWorldDef& FDescentWorldDef::Get() { static const TSharedRef Instance = BuildNew(&GWorldDefBuildMs); return *Instance; } double FDescentWorldDef::GetBuildMs() { return GWorldDefBuildMs; } TSharedRef FDescentWorldDef::BuildNew(double* OutMs) { const double T0 = FPlatformTime::Seconds(); TSharedRef Def = MakeShared(); Def->Build(); if (OutMs) { *OutMs = (FPlatformTime::Seconds() - T0) * 1000.0; } return Def; } // --------------------------------------------------------------------------------------------------------- // Building the definition (sections 2.8, 2.9) // --------------------------------------------------------------------------------------------------------- void FDescentWorldDef::Build() { // ---- galleries (turtle paths, carrier scale) ---- { FTube B; B.Id = TEXT("B"); B.Name = TEXT("THE LONG GALLERY"); B.bBig = true; WorldDefTurtlePath(FVector(-20, -62, -105), -75.0, -7.0, 34.0, { { TEXT('s'), 120, 0 }, { TEXT('t'), 35, 240 }, { TEXT('s'), 140, 0 }, { TEXT('t'), 20, 240 }, { TEXT('s'), 60, 0 } }, B.Pts); Tubes.Add(MoveTemp(B)); FTube S; S.Id = TEXT("S"); S.Name = TEXT("THE HAIRPIN"); S.bBig = true; WorldDefTurtlePath(FVector(900, -230, -765), 0.0, -9.5, 34.0, { { TEXT('s'), 380, 0 }, { TEXT('t'), -180, 150 }, { TEXT('s'), 301, 0 } }, S.Pts); Tubes.Add(MoveTemp(S)); } for (FTube& T : Tubes) { T.Cum.Reset(); T.Cum.Add(0.0); for (int32 I = 1; I < T.Pts.Num(); ++I) { const FVector A(T.Pts[I - 1]), B(T.Pts[I]); T.Cum.Add(T.Cum.Last() + FVector::Dist(A, B)); } T.Len = T.Cum.Last(); for (int32 I = 1; I < T.Pts.Num(); ++I) { FSegment S; S.A = FVector(T.Pts[I - 1]); S.B = FVector(T.Pts[I]); S.Ra = T.Pts[I - 1].W; S.Rb = T.Pts[I].W; S.bBig = true; S.Tube = Tubes.IndexOfByPredicate([&](const FTube& Q) { return Q.Id == T.Id; }); Segs.Add(S); } } // ---- Tender tubes: Catmull-Rom through the points, radius by smoothstep (section 2.9) ---- struct FTenderTubeDef { const TCHAR* Id; const TCHAR* Name; TArray Pts; }; const TArray TenderTubes = { { TEXT("E1"), TEXT("EAST SPUR"), { FVector4(30, 78, -62, 12), FVector4(45, 125, -72, 10), FVector4(60, 155, -78, 9), FVector4(68, 172, -84, 12) } }, { TEXT("E2"), TEXT("WEST SPUR"), { FVector4(70, -45, -95, 11), FVector4(85, -115, -104, 9), FVector4(105, -140, -108, 9) } }, { TEXT("C"), TEXT("WRECK HOLLOW"), { FVector4(260, 80, -165, 18), FVector4(285, 150, -178, 18), FVector4(310, 200, -190, 17), FVector4(330, 235, -200, 22) } }, { TEXT("X"), TEXT("THE CRACK"), { FVector4(375, 280, -208, 3.6), FVector4(392, 300, -214, 3.3), FVector4(410, 322, -224, 3.4), FVector4(424, 340, -232, 4.2) } }, { TEXT("D"), TEXT("THE CRAWL"), { FVector4(450, -40, -210, 5.5), FVector4(440, -130, -226, 4.6), FVector4(455, -210, -218, 5.0), FVector4(430, -290, -208, 5.2), FVector4(415, -345, -200, 6.5) } }, { TEXT("LG"), TEXT("GROTTO PASSAGE"), { FVector4(620, 40, -240, 9), FVector4(650, 110, -252, 8), FVector4(670, 160, -262, 10) } }, { TEXT("CH"), TEXT("THE CHIMNEYS"), { FVector4(330, -520, -245, 6), FVector4(318, -535, -380, 5), FVector4(345, -510, -500, 5.5), FVector4(372, -478, -560, 6) } }, { TEXT("E3"), TEXT("BLIND GALLERY"), { FVector4(700, 60, -705, 14), FVector4(735, 120, -712, 12), FVector4(760, 165, -716, 11) } }, { TEXT("SV"), TEXT("SIEVE MOUTH"), { FVector4(470, -450, -770, 8), FVector4(440, -530, -795, 7), FVector4(430, -620, -820, 9) } }, { TEXT("SD"), TEXT("SIEVE DRAIN"), { FVector4(640, -600, -1000, 7), FVector4(720, -540, -1020, 8) } }, { TEXT("UP"), TEXT("THE UNDERPASS"), { FVector4(1080, -320, -1062, 7), FVector4(1160, -262, -1140, 6), FVector4(1225, -232, -1215, 6), FVector4(1258, -240, -1262, 8) } }, }; for (const FTenderTubeDef& Def : TenderTubes) { FTube T; T.Id = Def.Id; T.Name = Def.Name; T.bBig = false; T.Pts = Def.Pts; const int32 TubeIndex = Tubes.Num(); TArray P3; for (const FVector4& P : Def.Pts) { P3.Add(FVector(P)); } // Arc-length table of the curve (three.js getLengths(200)), so points are evenly spaced along it. const int32 Div = WTune::TubeArcDivisions; TArray Lengths; Lengths.Add(0.0); FVector Prev = WorldDefCatmullRom(P3, 0.0); for (int32 I = 1; I <= Div; ++I) { const FVector Cur = WorldDefCatmullRom(P3, static_cast(I) / Div); Lengths.Add(Lengths.Last() + FVector::Dist(Cur, Prev)); Prev = Cur; } const double Total = Lengths.Last(); auto PointAt = [&](double U) { const double Target = U * Total; int32 Lo = 0, Hi = Div; while (Lo < Hi) { const int32 Mid = (Lo + Hi) / 2; if (Lengths[Mid] < Target) { Lo = Mid + 1; } else { Hi = Mid; } } int32 I = FMath::Max(Lo, 1); const double Seg = Lengths[I] - Lengths[I - 1]; const double Frac = Seg > 0.0 ? (Target - Lengths[I - 1]) / Seg : 0.0; return WorldDefCatmullRom(P3, (I - 1 + Frac) / Div); }; auto RadiusAt = [&](double U) { const double F = U * (Def.Pts.Num() - 1); const int32 I = FMath::Min(FMath::FloorToInt(F), Def.Pts.Num() - 2); const double K = F - I; return Descent::Lerp(Def.Pts[I].W, Def.Pts[I + 1].W, K * K * (3.0 - 2.0 * K)); }; const int32 N = FMath::Max(4, FMath::CeilToInt(Total / WTune::TubeSegmentLength)); FVector PrevP = PointAt(0.0); double PrevR = RadiusAt(0.0); for (int32 I = 1; I <= N; ++I) { const double U = static_cast(I) / N; const FVector Cur = PointAt(U); const double R = RadiusAt(U); FSegment S; S.A = PrevP; S.B = Cur; S.Ra = PrevR; S.Rb = R; S.bBig = false; S.Tube = TubeIndex; Segs.Add(S); PrevP = Cur; PrevR = R; } T.Len = Total; Tubes.Add(MoveTemp(T)); } // ---- chambers (section 2.8, 2.9) ---- auto AddChamber = [&](const TCHAR* Id, FVector C, FVector R, double K, bool bHonest = false) { FChamber Ch; Ch.Id = Id; Ch.C = C; Ch.R = R; Ch.K = K; Ch.bHonest = bHonest; Chambers.Add(Ch); }; AddChamber(TEXT("H1"), FVector(400, -470, -200), FVector(150, 130, 62), 10); AddChamber(TEXT("H2"), FVector(640, -230, -700), FVector(360, 320, 140), 12); AddChamber(TEXT("H3"), FVector(900, -420, -1020), FVector(260, 220, 95), 12); AddChamber(TEXT("G"), FVector(1250, -300, -1330), FVector(300, 240, 90), 12); AddChamber(TEXT("DEAD"), FVector(345, 250, -204), FVector(46, 40, 30), 8); AddChamber(TEXT("LANTERN"), FVector(690, 205, -268), FVector(60, 55, 34), 8); AddChamber(TEXT("HONEST"), FVector(436, 356, -236), FVector(26, 26, 20), 5, true); AddChamber(TEXT("HONEST2"), FVector(520, -760, -900), FVector(30, 30, 22), 5, true); NumDesignedChambers = Chambers.Num(); // ---- wells (radius 80) ---- auto AddWell = [&](const TCHAR* Id, double X, double Y, double Z1, double Z0) { FWell W; W.Id = Id; W.X = X; W.Y = Y; W.R = 80.0; W.Z0 = Z0; W.Z1 = Z1; Wells.Add(W); }; AddWell(TEXT("WA"), 760, -25, -250, -665); AddWell(TEXT("WB"), 430, -420, -200, -700); AddWell(TEXT("WC"), 820, -330, -800, -950); AddWell(TEXT("WD"), 1060, -380, -1080, -1280); // ---- the chokes ---- { FShelf SA; SA.Id = TEXT("SA"); SA.X = 760; SA.Y = -25; SA.Zc = -350; SA.T = 3; SA.Rclip = 88; SA.Axis = FVector2D(1, 0); SA.Half = 52; SA.W = 14; SA.Crack = { FVector(760, 35, -339), FVector(764, 39, -350), FVector(758, 43, -361) }; SA.CrackR = 3.4; Shelves.Add(SA); FShelf SC; SC.Id = TEXT("SC"); SC.X = 820; SC.Y = -330; SC.Zc = -880; SC.T = 3; SC.Rclip = 88; const double A = FMath::DegreesToRadians(-79.0); SC.Axis = FVector2D(FMath::Cos(A), FMath::Sin(A)); SC.Half = 50; SC.W = 13; SC.Crack = { FVector(881, -318, -869), FVector(885, -322, -880), FVector(879, -326, -891) }; SC.CrackR = 3.3; Shelves.Add(SC); } { auto AddPinch = [&](const TCHAR* Id, int32 TubeIndex, double S, double Half, double Ax, double Ay) { FPinch P; P.Id = Id; P.Tube = TubeIndex; P.S = S; P.Half = Half; P.Ax = Ax; P.Ay = Ay; P.TubeR = Tubes[TubeIndex].Pts[0].W; const FTubeSample Q = TubeAtS(TubeIndex, S); P.C = Q.P; P.Dir = Q.Dir; P.Side = FVector(-P.Dir.Y, P.Dir.X, 0.0).GetSafeNormal(); P.Up = FVector::CrossProduct(P.Side, P.Dir).GetSafeNormal(); if (P.Up.Z < 0.0) { P.Up = -P.Up; } Pinches.Add(P); }; AddPinch(TEXT("NEEDLE"), FindTube(TEXT("B")), 330.0, 7.0, 26.0, 15.0); AddPinch(TEXT("HTHROAT"), FindTube(TEXT("S")), 950.0, 6.0, 12.0, 12.0); } { FRing B; B.Id = TEXT("BRING"); B.Well = FindWell(TEXT("WB")); B.Zc = -450; B.Half = 7; B.RInner = 56; Rings.Add(B); FRing D; D.Id = TEXT("DRING"); D.Well = FindWell(TEXT("WD")); D.Zc = -1180; D.Half = 7; D.RInner = 48; Rings.Add(D); } // ---- the Sieve (sponge zone) ---- { FSpongeZone Z; Z.Id = TEXT("SIEVE"); Z.Box = FBox(FVector(300, -900, -1010), FVector(760, -560, -790)); Sponges.Add(Z); } RebuildGrid(); // ---- pores (seed 99): 900 spheres on the walls of H1, H2, H3, G and of Tender tubes below Z -330 ---- { Descent::FRng R(WTune::PoreSeed); TArray DeepTenderSegs; for (int32 I = 0; I < Segs.Num(); ++I) { if (!Segs[I].bBig && Segs[I].A.Z < WTune::PoreBelowZ) { DeepTenderSegs.Add(I); } } for (int32 Attempt = 0; Attempt < WTune::PoreMaxAttempts && Pores.Num() < WTune::PoreCount; ++Attempt) { const bool bChamber = R.Next() < 0.5 || DeepTenderSegs.Num() == 0; FVector C; double Wall = 0.0; FVector D(R.Next() * 2.0 - 1.0, R.Next() * 2.0 - 1.0, R.Next() * 1.6 - 0.8); D = D.GetSafeNormal(); const double Rad = WTune::PoreRadiusMin + R.Next() * WTune::PoreRadiusSpan; const double Out = Rad * (0.1 + R.Next() * 0.7); if (bChamber) { const FChamber& Ch = Chambers[R.Int(4)]; C = Ch.C; // Distance from the centre to the ellipsoid's surface along D. const double Q = FMath::Square(D.X / Ch.R.X) + FMath::Square(D.Y / Ch.R.Y) + FMath::Square(D.Z / Ch.R.Z); Wall = 1.0 / FMath::Sqrt(Q); } else { const FSegment& S = Segs[DeepTenderSegs[R.Int(DeepTenderSegs.Num())]]; C = S.A; Wall = S.Ra; } const FVector Pp = C + D * (Wall + Out); if (CarrierRouteDistance(Pp) < WTune::NearCarrierRouteM + Rad) { continue; } FPore Pore; Pore.C = Pp; Pore.R = Rad; Pores.Add(Pore); } } RebuildGrid(); // ---- worm tunnels (seed 5150): random walks into the rock from the walls of six chambers ---- { Descent::FRng R(WTune::WormSeed); const FName Sources[] = { TEXT("H1"), TEXT("H2"), TEXT("H3"), TEXT("G"), TEXT("DEAD"), TEXT("LANTERN") }; const int32 FirstWorm = Segs.Num(); for (int32 Attempt = 0; Attempt < WTune::WormCount * 12 && NumWorms < WTune::WormCount; ++Attempt) { const FChamber& Ch = Chambers[FindChamber(Sources[R.Int(6)])]; FVector D(R.Next() * 2.0 - 1.0, R.Next() * 2.0 - 1.0, R.Next() * 1.6 - 0.8); D = D.GetSafeNormal(); const double Q = FMath::Square(D.X / Ch.R.X) + FMath::Square(D.Y / Ch.R.Y) + FMath::Square(D.Z / Ch.R.Z); const FVector Wall = Ch.C + D * (1.0 / FMath::Sqrt(Q)); const FVector Nrm = FVector(D.X / Ch.R.X / Ch.R.X, D.Y / Ch.R.Y / Ch.R.Y, D.Z / Ch.R.Z / Ch.R.Z).GetSafeNormal(); const double Rad = R.Range(WTune::WormRadiusMin, WTune::WormRadiusMax); const double Length = R.Range(WTune::WormLengthMin, WTune::WormLengthMax); const int32 Steps = FMath::Max(2, FMath::RoundToInt(Length / WTune::WormStepM)); double Yaw = FMath::Atan2(Nrm.Y, Nrm.X); double Pitch = FMath::Asin(FMath::Clamp(Nrm.Z, -1.0, 1.0)); TArray Pts; FVector Pos = Wall - Nrm * 1.0; // start a metre inside the open side so the mouth is clean Pts.Add(Pos); bool bOk = true; for (int32 I = 0; I < Steps && bOk; ++I) { Yaw += FMath::DegreesToRadians((R.Next() * 2.0 - 1.0) * WTune::WormYawJitterDeg); Pitch = FMath::Clamp(Pitch + FMath::DegreesToRadians((R.Next() * 2.0 - 1.0) * WTune::WormPitchJitterDeg), -0.9, 0.9); Pos += FVector(FMath::Cos(Yaw) * FMath::Cos(Pitch), FMath::Sin(Yaw) * FMath::Cos(Pitch), FMath::Sin(Pitch)) * WTune::WormStepM; Pts.Add(Pos); if (CarrierRouteDistance(Pos) < WTune::NearCarrierRouteM + Rad) { bOk = false; } } bool bPocket = R.Next() < WTune::WormPocketChance; FVector PocketR(R.Range(WTune::WormPocketRadiusMin, WTune::WormPocketRadiusMax), R.Range(WTune::WormPocketRadiusMin, WTune::WormPocketRadiusMax), R.Range(WTune::WormPocketRadiusMin, WTune::WormPocketRadiusMax)); if (bOk && bPocket && CarrierRouteDistance(Pts.Last()) < WTune::NearCarrierRouteM + PocketR.GetMax()) { bOk = false; } if (!bOk) { continue; } for (int32 I = 1; I < Pts.Num(); ++I) { FSegment S; S.A = Pts[I - 1]; S.B = Pts[I]; S.Ra = Rad; S.Rb = Rad; S.bBig = false; S.Tube = -1; Segs.Add(S); } if (bPocket) { FChamber Pk; Pk.Id = FName(*FString::Printf(TEXT("POCKET%d"), NumPockets)); Pk.C = Pts.Last(); Pk.R = PocketR; Pk.K = WTune::WormPocketBlendK; Chambers.Add(Pk); ++NumPockets; } ++NumWorms; } (void)FirstWorm; } RebuildGrid(); // ---- deposits (seed 4242): capsule veins embedded in walls (section 2.12, v2 buildDeposits) ---- { Descent::FRng R(WTune::DepositSeed); TArray Sources; for (const FSegment& S : Segs) { if (R.Next() < 0.5) { Sources.Add((S.A + S.B) * 0.5); } } for (int32 CI = 0; CI < NumDesignedChambers; ++CI) { const FChamber& C = Chambers[CI]; for (int32 I = 0; I < 30; ++I) { Sources.Add(C.C + FVector((R.Next() - 0.5) * C.R.X, (R.Next() - 0.5) * C.R.Y, (R.Next() - 0.5) * C.R.Z)); } } for (double X = WTune::CanyonX0; X <= WTune::CanyonX1; X += 15.0) { Sources.Add(FVector(X, FDescentSDF::CanyonCentre(X) + (R.Next() - 0.5) * 40.0, FDescentSDF::CanyonFloor(X) + 8.0 + 30.0 * R.Next())); } for (const FShelf& S : Shelves) { for (int32 I = 0; I < 5; ++I) { Sources.Add(FVector(S.X + (R.Next() - 0.5) * 60.0, S.Y + (R.Next() - 0.5) * 60.0, S.Zc + 12.0 + (R.Next() - 0.5) * 6.0)); } } for (const FPinch& P : Pinches) { for (int32 I = 0; I < 4; ++I) { Sources.Add(P.C + FVector((R.Next() - 0.5) * 30.0, (R.Next() - 0.5) * 30.0, (R.Next() - 0.5) * 20.0)); } } for (const FWell& W : Wells) { for (int32 I = 0; I < 8; ++I) { const double A = R.Next() * 6.283185307, Rr = W.R * 0.6; Sources.Add(FVector(W.X + FMath::Cos(A) * Rr, W.Y + FMath::Sin(A) * Rr, Descent::Lerp(W.Z0 + 10.0, W.Z1 - 10.0, R.Next()))); } } for (int32 CI = NumDesignedChambers; CI < Chambers.Num(); ++CI) { for (int32 I = 0; I < 3; ++I) { Sources.Add(Chambers[CI].C + FVector((R.Next() - 0.5) * Chambers[CI].R.X, (R.Next() - 0.5) * Chambers[CI].R.Y, (R.Next() - 0.5) * Chambers[CI].R.Z)); } } FPrimList L; int32 Tries = 0; while (Deposits.Num() < WTune::DepositTarget && Tries++ < WTune::DepositMaxTries) { const FVector O = Sources[R.Int(Sources.Num())]; SdfObj.Gather(O, 6.0, L); if (SdfObj.GenRaw(O, L) > -1.0) { continue; } FVector D(R.Next() * 2.0 - 1.0, R.Next() * 2.0 - 1.0, R.Next() * 1.6 - 0.9); D = D.GetSafeNormal(); SdfObj.Gather(O + D * (WTune::DepositMarchMax * 0.5), WTune::DepositMarchMax * 0.5 + 6.0, L); double T = 0.0, Hit = -1.0; for (int32 I = 0; I < 200 && T < WTune::DepositMarchMax; ++I) { const double V = SdfObj.GenRaw(O + D * T, L); if (V > 0.0) { Hit = T; break; } T += FMath::Max(0.6, -V * 0.8); } if (Hit < 0.0) { continue; } const FVector Pt = O + D * Hit; if (Pt.Z > WTune::RimZ - WTune::DepositAboveRimSkip) { continue; } const FVector Grad = SdfObj.GradientRaw(Pt, L); // points into rock const FVector Nrm = -Grad; // outward normal const FVector Tan = FVector::CrossProduct(Nrm, FVector(R.Next() - 0.5, R.Next() - 0.5, R.Next() - 0.5)).GetSafeNormal(); const double Half = WTune::DepositHalfMin + R.Next() * WTune::DepositHalfSpan; const double Rad = WTune::DepositRadiusMin + R.Next() * WTune::DepositRadiusSpan; const FVector Centre = Pt - Nrm * (Rad * WTune::DepositSinkFactor); const double Rt = R.Next(); EOreType Type; if (Pt.Z > -370.0) { Type = Rt < 0.92 ? EOreType::Ferrite : EOreType::Cobalt; } else if (Pt.Z > -970.0) { Type = Rt < 0.40 ? EOreType::Ferrite : (Rt < 0.93 ? EOreType::Cobalt : EOreType::Pearl); } else { Type = Rt < 0.15 ? EOreType::Ferrite : (Rt < 0.55 ? EOreType::Cobalt : EOreType::Pearl); } bool bTooClose = false; for (const FDeposit& Other : Deposits) { if (FVector::DistSquared(Other.P, Pt) < WTune::DepositMinSpacing * WTune::DepositMinSpacing) { bTooClose = true; break; } } if (bTooClose) { continue; } FDeposit Dep; Dep.A = Centre - Tan * Half; Dep.B = Centre + Tan * Half; Dep.R = Rad; Dep.Type = Type; Dep.N = Nrm; Dep.P = Pt; Dep.Yield = FMath::RoundToInt(WTune::DepositYieldBase + Half * Rad * WTune::DepositYieldFactor); Deposits.Add(Dep); } } RebuildGrid(); // ---- beacons (section 2.9) ---- auto AddBeacon = [&](const TCHAR* Id, const TCHAR* Name, const TCHAR* Who, FVector P, const TCHAR* Text) { FBeacon B; B.Id = Id; B.Name = Name; B.Who = Who; B.P = P; B.Text = Text; Beacons.Add(B); }; AddBeacon(TEXT("b1"), TEXT("CONTRACTOR H-19877"), TEXT("H-19877 - PERSONAL LOG"), FVector(350, 255, -222), TEXT("Day forty. The Hollow goes nowhere, but the sonar keeps showing a crack on the east wall. Readings past it are wrong - full pressure, like the charts say it should be everywhere. I'm going to widen it and have a look, Tethys pays double for honest-void surveys if you")); AddBeacon(TEXT("b2"), TEXT("SURVEY PROBE 2193"), TEXT("PROBE HDR-01 - BLACK BOX"), FVector(338, 240, -224), TEXT("PING 0417 - RETURN 28m04s - RETURN AMPLITUDE EXCEEDS OUTGOING BY 3.2 dB - REPEAT - RETURN AMPLITUDE EXCEEDS")); AddBeacon(TEXT("b3"), TEXT("CONTRACTOR H-20114"), TEXT("H-20114 - DISTRESS"), FVector(448, -175, -222), TEXT("Crawl's too narrow to turn in. Power at nine percent. Something passed the mouth of the tunnel behind me, it blocked the light for a full minute, it was so long, I don't think it")); { const FPinch& Ht = Pinches[1]; AddBeacon(TEXT("b4"), TEXT("CONTRACTOR H-18830"), TEXT("H-18830 - CARRIER LOG"), Ht.C - Ht.Dir * 12.0, TEXT("The Hairpin pinches here. Carrier can't pass until we cut it wider. The heartbeat is louder down here. The crew says they can hear it in the hull even with the engines off. Tonight it sounded like it was coming from")); } AddBeacon(TEXT("b5"), TEXT("CONTRACTOR H-20390"), TEXT("H-20390 - TETHYS LINK"), FVector(820, -330, -872), TEXT("Dispatch, the Drop is collapsed below the Void. We have a Tender down to cut it. Requesting confirmation that the thing on sonar at four hundred metres is a geological artefact and not")); AddBeacon(TEXT("b6"), TEXT("DEPTH STATION KILO-7"), TEXT("KILO-7 - AUTOMATIC"), FVector(560, -330, -826), TEXT("KILO-7 automatic. Station population: zero. Last docking: four hundred and eleven days. Fuel, air and trade remain available to contract holders. Please do not attempt to answer the knocking on the lower hatch. Tethys reminds you that the knocking is")); AddBeacon(TEXT("b7"), TEXT("PILGRIM-CLASS ANTIPHON"), TEXT("ANTIPHON - CARRIER LOG"), FVector(980, -520, -1105), TEXT("ANTIPHON carrier log. We cut the Drop wide enough and she went through like a hymn. Hull at sixty percent. The crew wants to hold here but the stratum is right under us, I can see the well. Something came up it an hour ago and has not gone back down. It is still under the")); AddBeacon(TEXT("b8"), TEXT("CONTRACTOR H-17002"), TEXT("H-17002 - TETHYS LINK"), FVector(500, -740, -895), TEXT("The Sieve is all pearl. I have a full hold and I cannot find the way I came in. The sonar says there is a big room behind this wall, pressure reads wrong, reads honest. If I cut through I can use it to turn around. Cutting now. The wall is")); // A designed landmark position that the noise buried in rock (the floor of the Undercroft, the small honest void) is moved // toward the nearest chamber's centre until it is in open water, so a beacon is never inside the wall. for (FBeacon& B : Beacons) { if (SdfObj.GenRawSlow(B.P) < -1.0) { continue; } int32 Best = 0; double BestD = 1e30; for (int32 CI = 0; CI < NumDesignedChambers; ++CI) { const FChamber& C = Chambers[CI]; const double Q = FMath::Square((B.P.X - C.C.X) / C.R.X) + FMath::Square((B.P.Y - C.C.Y) / C.R.Y) + FMath::Square((B.P.Z - C.C.Z) / C.R.Z); if (Q < BestD) { BestD = Q; Best = CI; } } const FVector Dir = (Chambers[Best].C - B.P).GetSafeNormal(); for (int32 Step = 0; Step < 160 && SdfObj.GenRawSlow(B.P) > -2.0; ++Step) { B.P += Dir * 0.5; } } // ---- named places (Descent.World.Goto, the tour scenarios) ---- auto AddPlace = [&](const TCHAR* Name, FVector Pos, FVector Look) { FPlace P; P.Name = Name; P.PosM = Pos; P.LookAtM = Look; Places.Add(P); }; AddPlace(TEXT("start"), FVector(0, 0, -30), FVector(120, -10, -60)); AddPlace(TEXT("canyon_mid"), FVector(425, 25, -120), FVector(520, 25, -160)); AddPlace(TEXT("wellA"), FVector(700, -25, -300), FVector(760, -25, -330)); AddPlace(TEXT("shelfA"), FVector(730, -40, -330), FVector(760, -25, -350)); { const FPinch& N = Pinches[0]; AddPlace(TEXT("needle"), N.C - N.Dir * 14.0, N.C); } AddPlace(TEXT("h1"), FVector(400, -470, -200), FVector(520, -470, -215)); AddPlace(TEXT("bore"), FVector(430, -420, -420), FVector(430, -420, -460)); AddPlace(TEXT("void"), FVector(640, -230, -700), FVector(740, -230, -725)); AddPlace(TEXT("kilo7"), FVector(540, -330, -800), FVector(560, -330, -826)); AddPlace(TEXT("drop"), FVector(800, -330, -850), FVector(820, -330, -880)); AddPlace(TEXT("shelfC"), FVector(790, -300, -860), FVector(820, -330, -880)); AddPlace(TEXT("hairpin"), FVector(900, -230, -765), FVector(1000, -230, -790)); AddPlace(TEXT("undercroft"), FVector(900, -420, -1020), FVector(980, -480, -1090)); AddPlace(TEXT("antiphon"), FVector(960, -500, -1085), FVector(980, -520, -1105)); AddPlace(TEXT("gullet"), FVector(1060, -380, -1180), FVector(1060, -380, -1220)); AddPlace(TEXT("goal"), FVector(1250, -300, -1330), FVector(1350, -300, -1335)); AddPlace(TEXT("abyss"), FVector(1250, -300, -1500), FVector(1250, -300, -1600)); AddPlace(TEXT("hollow"), FVector(345, 250, -204), FVector(355, 262, -226)); AddPlace(TEXT("honest"), FVector(436, 356, -236), FVector(452, 356, -236)); AddPlace(TEXT("grotto"), FVector(690, 205, -268), FVector(710, 205, -272)); AddPlace(TEXT("sieve"), FVector(470, -450, -770), FVector(440, -530, -795)); AddPlace(TEXT("spur_e1"), FVector(30, 78, -62), FVector(60, 155, -78)); ComputeHash(); } // --------------------------------------------------------------------------------------------------------- // Spatial index // --------------------------------------------------------------------------------------------------------- void FDescentWorldDef::RebuildGrid() { Grid = FGrid(); BigSegIndices.Reset(); for (int32 I = 0; I < Segs.Num(); ++I) { const FSegment& S = Segs[I]; const double M = FMath::Max(S.Ra, S.Rb) + WTune::GatherSegMargin + 1.0; WorldDefInsertBox(Grid.Segs, FVector(FMath::Min(S.A.X, S.B.X), FMath::Min(S.A.Y, S.B.Y), FMath::Min(S.A.Z, S.B.Z)) - FVector(M), FVector(FMath::Max(S.A.X, S.B.X), FMath::Max(S.A.Y, S.B.Y), FMath::Max(S.A.Z, S.B.Z)) + FVector(M), I); if (S.bBig) { BigSegIndices.Add(I); } } for (int32 I = 0; I < Chambers.Num(); ++I) { const FChamber& C = Chambers[I]; const double M = C.R.GetMax() + WTune::GatherChamberMargin + 1.0; WorldDefInsertBox(Grid.Chambers, C.C - FVector(M), C.C + FVector(M), I); } for (int32 I = 0; I < Pores.Num(); ++I) { const double M = Pores[I].R + WTune::GatherPoreMargin + 1.0; WorldDefInsertBox(Grid.Pores, Pores[I].C - FVector(M), Pores[I].C + FVector(M), I); } for (int32 I = 0; I < Deposits.Num(); ++I) { WorldDefInsertBox(Grid.Deposits, Deposits[I].P - FVector(WTune::DepositMarchMax), Deposits[I].P + FVector(WTune::DepositMarchMax), I); } } void FDescentWorldDef::GatherInto(const FVector& C, double Rad, FPrimList& Out) const { Out.Reset(); TArray> Idx; WorldDefCollect(Grid.Segs, C, Rad, Idx); for (const int32 I : Idx) { if (FDescentSDF::SegDistR(Segs[I], C.X, C.Y, C.Z) < Rad + WTune::GatherSegMargin) { Out.Segs.Add(&Segs[I]); } } WorldDefCollect(Grid.Chambers, C, Rad, Idx); for (const int32 I : Idx) { const FChamber& Ch = Chambers[I]; if (FVector::Dist(C, Ch.C) < Rad + Ch.R.GetMax() + WTune::GatherChamberMargin) { Out.Chambers.Add(&Ch); } } WorldDefCollect(Grid.Pores, C, Rad, Idx); for (const int32 I : Idx) { if (FVector::Dist(C, Pores[I].C) < Rad + Pores[I].R + WTune::GatherPoreMargin) { Out.Pores.Add(&Pores[I]); } } for (const FWell& W : Wells) { if (FMath::Sqrt(FMath::Square(C.X - W.X) + FMath::Square(C.Y - W.Y)) < W.R + Rad + WTune::GatherWellMargin && C.Z > W.Z0 - Rad - WTune::GatherWellMargin && C.Z < W.Z1 + Rad + WTune::GatherWellMargin) { Out.Wells.Add(&W); } } for (const FShelf& S : Shelves) { if (FMath::Sqrt(FMath::Square(C.X - S.X) + FMath::Square(C.Y - S.Y)) < S.Rclip + Rad + WTune::GatherShelfMargin && FMath::Abs(C.Z - S.Zc) < Rad + S.T + 10.0) { Out.Shelves.Add(&S); } } for (const FPinch& P : Pinches) { if (FVector::Dist(C, P.C) < Rad + WTune::GatherPinchMargin) { Out.Pinches.Add(&P); } } for (const FRing& R : Rings) { const FWell& W = Wells[R.Well]; if (FMath::Sqrt(FMath::Square(C.X - W.X) + FMath::Square(C.Y - W.Y)) < W.R + Rad + WTune::GatherWellMargin && FMath::Abs(C.Z - R.Zc) < Rad + R.Half + 10.0) { Out.Rings.Add(&R); } } for (const FSpongeZone& Z : Sponges) { // The sponge is not a distance field: take it whenever the region comes near its box. if (FDescentSDF::BoxSDF(Z.Box, C.X, C.Y, C.Z) < Rad + WTune::SpongeBoxGrow + WTune::SpongeBlendK + 2.0) { Out.Sponges.Add(&Z); } } // The canyon term reaches anywhere near its box, and the rim term (the sea floor beyond) anywhere near Z = +40. const double M = Rad + WTune::CanyonBlendK + 4.0; const bool bInBox = C.X > WTune::CanyonX0 - M && C.X < WTune::CanyonX1 + M && C.Y > -180.0 - M && C.Y < 180.0 + M && C.Z > -275.0 - M && C.Z < WTune::RimZ + WTune::RimWobble + M; Out.bCanyon = bInBox || (C.Z + Rad > WTune::RimZ - WTune::RimWobble - 60.0); Out.bAbyss = FMath::Sqrt(FMath::Square(C.X - WTune::AbyssX) + FMath::Square(C.Y - WTune::AbyssY)) < Rad + WTune::AbyssRadiusTop + WTune::AbyssBlendK + 8.0 && C.Z < WTune::AbyssTopZ + Rad + WTune::AbyssBlendK + 2.0 && C.Z > WTune::AbyssBottomZ - Rad - WTune::AbyssBlendK - 2.0; } // --------------------------------------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------------------------------------- int32 FDescentWorldDef::FindTube(FName Id) const { return Tubes.IndexOfByPredicate([&](const FTube& T) { return T.Id == Id; }); } int32 FDescentWorldDef::FindWell(FName Id) const { return Wells.IndexOfByPredicate([&](const FWell& W) { return W.Id == Id; }); } int32 FDescentWorldDef::FindChamber(FName Id) const { return Chambers.IndexOfByPredicate([&](const FChamber& C) { return C.Id == Id; }); } const FPlace* FDescentWorldDef::FindPlace(FName Name) const { return Places.FindByPredicate([&](const FPlace& P) { return P.Name == Name; }); } FTubeSample FDescentWorldDef::TubeAtS(int32 Tube, double S) const { FTubeSample Out; if (!Tubes.IsValidIndex(Tube) || Tubes[Tube].Cum.Num() < 2) { return Out; } const FTube& T = Tubes[Tube]; S = FMath::Clamp(S, 0.0, T.Len); int32 I = 1; while (I < T.Cum.Num() - 1 && T.Cum[I] < S) { ++I; } const FVector4& A = T.Pts[I - 1]; const FVector4& B = T.Pts[I]; const double K = (S - T.Cum[I - 1]) / FMath::Max(1e-6, T.Cum[I] - T.Cum[I - 1]); Out.P = FVector(Descent::Lerp(A.X, B.X, K), Descent::Lerp(A.Y, B.Y, K), Descent::Lerp(A.Z, B.Z, K)); Out.R = Descent::Lerp(A.W, B.W, K); const FVector D(B.X - A.X, B.Y - A.Y, B.Z - A.Z); Out.YawRad = FMath::Atan2(D.Y, D.X); Out.PitchRad = FMath::Atan2(D.Z, FMath::Sqrt(D.X * D.X + D.Y * D.Y)); Out.Dir = D.GetSafeNormal(); return Out; } double FDescentWorldDef::CarrierRouteDistance(const FVector& P) const { double D = 1e9; for (const FWell& W : Wells) { D = FMath::Min(D, FDescentSDF::WellSDF(W, P.X, P.Y, P.Z)); } for (const FSegment& S : Segs) { if (S.bBig) { D = FMath::Min(D, FDescentSDF::SegDistR(S, P.X, P.Y, P.Z)); } } return D; } double FDescentWorldDef::ClaimDist(const FVector& P) { static const FBox Claim(FVector(WTune::ClaimX0, WTune::ClaimY0, WTune::ClaimZ0), FVector(WTune::ClaimX1, WTune::ClaimY1, WTune::ClaimZ1)); return FDescentSDF::BoxSDF(Claim, P.X, P.Y, P.Z); } double FDescentWorldDef::HardnessAt(const FVector& P) { double H = P.Z > WTune::Stratum1Z ? WTune::HardnessS0 : (P.Z > WTune::Stratum2Z ? WTune::HardnessS1 : (P.Z > WTune::Stratum3Z ? WTune::HardnessS2 : WTune::HardnessS3)); H *= 1.0 + WTune::BasaltHardnessGain * FMath::Clamp(ClaimDist(P) / WTune::BasaltRampM, 0.0, 1.0); return H; } void FDescentWorldDef::DepositsNear(const FVector& Centre, double Radius, TArray>& Out) const { Out.Reset(); TArray> Idx; WorldDefCollect(Grid.Deposits, Centre, 1.0, Idx); for (const int32 I : Idx) { if (FVector::Dist(Deposits[I].P, Centre) < Radius + 8.0) { Out.Add(I); } } } int32 FDescentWorldDef::NearestDeposit(const FVector& P, int32 TypeFilter, double* OutDist) const { int32 Best = -1; double BestD = 1e30; for (int32 I = 0; I < Deposits.Num(); ++I) { if (TypeFilter >= 0 && static_cast(Deposits[I].Type) != TypeFilter) { continue; } const double D = FVector::DistSquared(Deposits[I].P, P); if (D < BestD) { BestD = D; Best = I; } } if (OutDist) { *OutDist = Best >= 0 ? FMath::Sqrt(BestD) : -1.0; } return Best; } int32 FDescentWorldDef::DepositAt(const FVector& P, TConstArrayView Candidates, double& OutK) const { for (const int32 I : Candidates) { const FDeposit& D = Deposits[I]; const double S = FDescentSDF::SegDist3(D.A, D.B, P.X, P.Y, P.Z); if (S < D.R) { OutK = 1.0 - S / D.R; return I; } } OutK = 0.0; return -1; } void FDescentWorldDef::ComputeHash() { FWorldDefFnv H; H.Int(Tubes.Num()); for (const FTube& T : Tubes) { H.Str(T.Id.ToString()); H.Int(T.bBig); for (const FVector4& P : T.Pts) { H.Num(P.X); H.Num(P.Y); H.Num(P.Z); H.Num(P.W); } } H.Int(Segs.Num()); for (const FSegment& S : Segs) { H.Vec(S.A); H.Vec(S.B); H.Num(S.Ra); H.Num(S.Rb); H.Int(S.bBig); } H.Int(Chambers.Num()); for (const FChamber& C : Chambers) { H.Vec(C.C); H.Vec(C.R); H.Num(C.K); } H.Int(Wells.Num()); for (const FWell& W : Wells) { H.Num(W.X); H.Num(W.Y); H.Num(W.R); H.Num(W.Z0); H.Num(W.Z1); } H.Int(Pores.Num()); for (const FPore& P : Pores) { H.Vec(P.C); H.Num(P.R); } H.Int(Shelves.Num()); for (const FShelf& S : Shelves) { H.Num(S.X); H.Num(S.Y); H.Num(S.Zc); H.Num(S.T); H.Num(S.Rclip); H.Num(S.Axis.X); H.Num(S.Axis.Y); H.Num(S.Half); H.Num(S.W); H.Num(S.CrackR); for (const FVector& V : S.Crack) { H.Vec(V); } } H.Int(Pinches.Num()); for (const FPinch& P : Pinches) { H.Vec(P.C); H.Vec(P.Dir); H.Num(P.Half); H.Num(P.Ax); H.Num(P.Ay); } H.Int(Rings.Num()); for (const FRing& R : Rings) { H.Int(R.Well); H.Num(R.Zc); H.Num(R.Half); H.Num(R.RInner); } H.Int(Sponges.Num()); for (const FSpongeZone& Z : Sponges) { H.Vec(Z.Box.Min); H.Vec(Z.Box.Max); } H.Int(Deposits.Num()); for (const FDeposit& D : Deposits) { H.Vec(D.A); H.Vec(D.B); H.Num(D.R); H.Int(static_cast(D.Type)); H.Int(D.Yield); } H.Int(Beacons.Num()); for (const FBeacon& B : Beacons) { H.Vec(B.P); H.Str(B.Text); } HashValue = H.H; } FString FDescentWorldDef::HashHex() const { return FString::Printf(TEXT("%016llx"), HashValue); } } // ------------------------------------------------------------------------------------------------------------ // State section "world_def" (DESIGN_UE.md section 9.4) // ------------------------------------------------------------------------------------------------------------ #include "Debug/DescentStateRegistry.h" static FDescentStateProviderRegistrar GWorldDefSection(TEXT("world_def"), [](UWorld*, FJsonObject& Out) { const Descent::World::FDescentWorldDef& Def = Descent::World::FDescentWorldDef::Get(); Out.SetNumberField(TEXT("seed"), Descent::Tune::World::NoiseSeedA); Out.SetNumberField(TEXT("segments"), Def.Segs.Num()); Out.SetNumberField(TEXT("chambers"), Def.Chambers.Num()); Out.SetNumberField(TEXT("designed_chambers"), Def.NumDesignedChambers); Out.SetNumberField(TEXT("wells"), Def.Wells.Num()); Out.SetNumberField(TEXT("pores"), Def.Pores.Num()); Out.SetNumberField(TEXT("worms"), Def.NumWorms); Out.SetNumberField(TEXT("pockets"), Def.NumPockets); Out.SetNumberField(TEXT("deposits"), Def.Deposits.Num()); Out.SetNumberField(TEXT("beacons"), Def.Beacons.Num()); Out.SetStringField(TEXT("hash"), Def.HashHex()); Out.SetNumberField(TEXT("build_ms"), Descent::Json::Round(Descent::World::FDescentWorldDef::GetBuildMs())); }); // Boot (section 1.5 step 1): build the world definition before anything asks for it. #include "Core/DescentBoot.h" #include "TheDescent.h" DESCENT_BOOT_STEP(BuildWorldDef, Descent::Boot::Order::BuildWorldDef) { const Descent::World::FDescentWorldDef& Def = Descent::World::FDescentWorldDef::Get(); UE_LOG(LogDescent, Display, TEXT("World: definition built in %.0f ms: %d segments, %d chambers, %d wells, %d pores, %d worms, %d deposits, hash %s"), Descent::World::FDescentWorldDef::GetBuildMs(), Def.Segs.Num(), Def.Chambers.Num(), Def.Wells.Num(), Def.Pores.Num(), Def.NumWorms, Def.Deposits.Num(), *Def.HashHex()); }