Skip to content

Commit 4d25750

Browse files
committed
Add implementations for FG WP runtime spatial hash
Add the implementations of `FGWorldPartitionRuntimeSpatialHash.cpp` and `FWPSaveDataMigrationContext.cpp`, kindly provided by Archengius. These should allow custom levels that use World Partition. The level must use the `FGWorldPartitionRuntimeSpatialHash` class (using the default class will result in a cast failure crash), but it was impossible to cook the level with the stubs we had in the project. Arch also provided `FWPSaveDataMigrationContext.cpp` as it gets used by `FGWorldPartitionRuntimeSpatialHash.cpp` to generate WP migration data, needed to support WP migration for modded WP worlds. TODO: I had to implement a function in FGObjectReference.cpp but I have no idea if the implementation is correct. Without this function's definition, the Arch-provided code does not build (linker error). Signed-off-by: Angel Pons <[email protected]>
1 parent 7ffcb7b commit 4d25750

File tree

3 files changed

+633
-23
lines changed

3 files changed

+633
-23
lines changed

Source/FactoryGame/Private/FGObjectReference.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ void FObjectReferenceDisc::GetRelativePath(const UObject* obj, FString& out_path
2121
const ULevel* FObjectReferenceDisc::FindOuterLevel(const UObject* obj){ return nullptr; }
2222
const UWorldPartitionRuntimeCell* FObjectReferenceDisc::FindWorldPartitionCell(const UWorld* world, const FString& levelName){ return nullptr; }
2323
ULevel* FObjectReferenceDisc::FindLevel(UWorld* world) const{ return nullptr; }
24+
FArchive& operator<<(FArchive& ar, FObjectReferenceDisc& reference)
25+
{
26+
ar << reference.LevelName;
27+
ar << reference.PathName;
28+
ar << reference.BlueprintRedirectCount;
29+
return ar;
30+
}
2431
bool FObjectReferenceDisc::Valid() const{ return bool(); }
2532
FString FObjectReferenceDisc::ToString() const{ return FString(); }
2633
void FObjectReferenceDisc::ClearRedirects(){ }
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,177 @@
1-
// This file has been automatically generated by the Unreal Header Implementation tool
1+
// Copyright Coffee Stain Studios. All Rights Reserved.
22

33
#include "FGWorldPartitionRuntimeSpatialHash.h"
4+
#include "FGWorldSettings.h"
5+
#include "FactoryGame.h"
6+
#include "WorldPartition/Cook/WorldPartitionCookPackage.h"
7+
#include "WorldPartition/RuntimeSpatialHash/RuntimeSpatialHashGridHelper.h"
48

59
#if WITH_EDITOR
6-
uint32 UFGWorldPartitionRuntimeSpatialHash::GetGridCellSize(FName GridName){ return uint32(); }
7-
bool UFGWorldPartitionRuntimeSpatialHash::PopulateGeneratorPackageForCook(const TArray<FWorldPartitionCookPackage*>& PackagesToCook, TArray<UPackage*>& OutModifiedPackage){ return bool(); }
8-
#endif
9-
bool UFGWorldPartitionRuntimeSpatialHash::InjectExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject){ return bool(); }
10-
bool UFGWorldPartitionRuntimeSpatialHash::RemoveExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject){ return bool(); }
11-
UWorldPartitionRuntimeCell* UFGWorldPartitionRuntimeSpatialHash::FindCellByName(FName cellName) const{ return nullptr; }
12-
bool UFGWorldPartitionRuntimeSpatialHash::IsCellContainingWorldLocationLoaded(const FName& GridName, const FVector& Location) const{ return bool(); }
13-
void UFGWorldPartitionRuntimeSpatialHash::RebuildNameToCellMap() const{ }
10+
uint32 UFGWorldPartitionRuntimeSpatialHash::GetGridCellSize( FName GridName )
11+
{
12+
uint32 CellSize = 0;
13+
ForEachStreamingGridBreakable( [&CellSize, GridName](const FSpatialHashStreamingGrid& Grid)
14+
{
15+
if(Grid.GridName == GridName)
16+
{
17+
CellSize = Grid.CellSize;
18+
return false;
19+
}
20+
return true;
21+
} );
22+
return CellSize;
23+
}
24+
25+
bool UFGWorldPartitionRuntimeSpatialHash::PopulateGeneratorPackageForCook( const TArray<FWorldPartitionCookPackage*>& PackagesToCook,
26+
TArray<UPackage*>& OutModifiedPackage )
27+
{
28+
const auto Result = Super::PopulateGeneratorPackageForCook( PackagesToCook, OutModifiedPackage );
29+
if(const auto* Level = GetWorld()->PersistentLevel.Get(); Level)
30+
{
31+
const auto* MainWorld = GetWorld();
32+
AFGWorldSettings* MainWorldSettings = Cast<AFGWorldSettings>(MainWorld->GetWorldSettings());
33+
FWPSaveDataMigrationContext::CollectSaveGameValidationDataForPersistentLevel( *Level, *MainWorldSettings );
34+
ForEachStreamingGrid( [MainWorldSettings](const FSpatialHashStreamingGrid& grid)
35+
{
36+
grid.ForEachRuntimeCell( [MainWorldSettings, &grid](const UWorldPartitionRuntimeCell* Cell)-> bool
37+
{
38+
FWPSaveDataMigrationContext::CollectSaveGameValidationData(grid, *Cast<const UWorldPartitionRuntimeLevelStreamingCell>(Cell), *MainWorldSettings);
39+
return true;
40+
} );
41+
} );
42+
43+
}
44+
45+
return Result;
46+
}
47+
48+
#endif
49+
bool UFGWorldPartitionRuntimeSpatialHash::InjectExternalStreamingObject( URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject )
50+
{
51+
if ( Super::InjectExternalStreamingObject( ExternalStreamingObject ) )
52+
{
53+
mNameToCellMapDirty = true;
54+
return true;
55+
}
56+
return false;
57+
}
58+
59+
bool UFGWorldPartitionRuntimeSpatialHash::RemoveExternalStreamingObject( URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject )
60+
{
61+
if ( Super::RemoveExternalStreamingObject(ExternalStreamingObject) )
62+
{
63+
mNameToCellMapDirty = true;
64+
return true;
65+
}
66+
return false;
67+
}
68+
69+
UWorldPartitionRuntimeCell* UFGWorldPartitionRuntimeSpatialHash::FindCellByName( FName cellName ) const
70+
{
71+
if ( mNameToCellMapDirty )
72+
{
73+
RebuildNameToCellMap();
74+
mNameToCellMapDirty = false;
75+
}
76+
UWorldPartitionRuntimeCell* const* cellMapEntry = mNameToCellMap.Find( cellName );
77+
return cellMapEntry ? *cellMapEntry : nullptr;
78+
}
79+
80+
bool UFGWorldPartitionRuntimeSpatialHash::IsCellContainingWorldLocationLoaded( const FName& GridName, const FVector& Location ) const
81+
{
82+
bool foundLoadedCell = false;
83+
84+
ForEachStreamingGridBreakable( [ GridName, Location, &foundLoadedCell ]( const FSpatialHashStreamingGrid& grid )-> bool
85+
{
86+
if( grid.GridName == GridName )
87+
{
88+
FVector2D Location2D = FVector2D( Location );
89+
const FSquare2DGridHelper& gridHelper = grid.GetGridHelper();
90+
91+
//Since we know input location, we can simply calculate the cell coordinates for each grid level.
92+
//Other FSpatialHashStreamingGrid methods use FULL walkthrough against all cells to find the cell containing the location/radius
93+
for( int32 GridLevel = 0; GridLevel < gridHelper.Levels.Num(); ++GridLevel )
94+
{
95+
FGridCellCoord2 CellCoords;
96+
if( gridHelper.Levels[ GridLevel ].GetCellCoords( Location2D, CellCoords ) )
97+
{
98+
FGridCellCoord Coords( CellCoords.X, CellCoords.Y, GridLevel );
99+
100+
check( grid.GridLevels.IsValidIndex(Coords.Z) );
101+
102+
const int64 CoordKey = Coords.Y * gridHelper.Levels[ Coords.Z ].GridSize + Coords.X;
103+
104+
if( const int32* LayerCellIndexPtr = grid.GridLevels[ Coords.Z ].LayerCellsMapping.Find( CoordKey ) )
105+
{
106+
const auto& runtimeCells = grid.GridLevels[ Coords.Z ].LayerCells[ *LayerCellIndexPtr ].GridCells;
107+
for( const UWorldPartitionRuntimeCell* OutCell: runtimeCells )
108+
{
109+
// Check if the X and Y components of the location are within the cell bounds
110+
if( OutCell->GetStreamingStatus() == EStreamingStatus::LEVEL_Visible )
111+
{
112+
foundLoadedCell = true;
113+
return false;
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
//okay, we didn't find it among the regular cells, now lets check always loaded and injected ones.
121+
//We don't have direct access to them, so just use standard api without spatial query
122+
FWorldPartitionStreamingQuerySource LocationQuery{ Location };
123+
LocationQuery.bSpatialQuery = false;
124+
TSet< const UWorldPartitionRuntimeCell* > OutCells;
125+
grid.GetCells( LocationQuery, OutCells, false );
126+
for( const UWorldPartitionRuntimeCell* OutCell: OutCells )
127+
{
128+
const FBox CellBounds = OutCell->GetCellBounds();
129+
// Check if the X and Y components of the location are within the cell bounds
130+
if( Location.X >= CellBounds.Min.X && Location.X <= CellBounds.Max.X )
131+
{
132+
if( Location.Y >= CellBounds.Min.Y && Location.Y <= CellBounds.Max.Y )
133+
{
134+
if( OutCell->GetStreamingStatus() == EStreamingStatus::LEVEL_Visible )
135+
{
136+
foundLoadedCell = true;
137+
return false;
138+
}
139+
}
140+
}
141+
}
142+
143+
return false;
144+
}
145+
146+
return true;
147+
} );
148+
149+
return foundLoadedCell;
150+
}
151+
152+
void UFGWorldPartitionRuntimeSpatialHash::RebuildNameToCellMap() const
153+
{
154+
mNameToCellMap.Empty();
155+
156+
// We cannot use ForEachStreamingCells because it will not consider cells that are "not relevant" according to the client-only visibility
157+
ForEachStreamingGrid([&]( const FSpatialHashStreamingGrid& streamingGrid )
158+
{
159+
for ( const FSpatialHashStreamingGridLevel& gridLevel : streamingGrid.GridLevels )
160+
{
161+
for ( const FSpatialHashStreamingGridLayerCell& layerCell : gridLevel.LayerCells )
162+
{
163+
for ( UWorldPartitionRuntimeCell* cell : layerCell.GridCells )
164+
{
165+
// Make sure cells do not have duplicate names. In theory they can be for cells that come from external objects (e.g. have different outers),
166+
// but that shouldn't really be the case as external object cells will have their Content Bundle ID encoded in their name
167+
if ( UWorldPartitionRuntimeCell* const* oldCell = mNameToCellMap.Find( cell->GetFName() ) )
168+
{
169+
fgcheckf( false, TEXT("World Partition Cells with Duplicate Names found: '%s' and '%s'"), *GetFullNameSafe( cell ), *GetFullNameSafe( *oldCell ) );
170+
}
171+
mNameToCellMap.Add( cell->GetFName(), cell );
172+
}
173+
}
174+
}
175+
});
176+
UE_LOG( LogGame, Log, TEXT("Built World Partition cell cache for World '%s', %d cells in cache."), *GetPathNameSafe( GetWorld() ), mNameToCellMap.Num() );
177+
}

0 commit comments

Comments
 (0)