From 4d25750dbc18928d765481a052a826144e201d02 Mon Sep 17 00:00:00 2001 From: Angel Pons Date: Fri, 24 Jan 2025 17:12:29 +0100 Subject: [PATCH] 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 --- .../FactoryGame/Private/FGObjectReference.cpp | 7 + .../FGWorldPartitionRuntimeSpatialHash.cpp | 182 ++++++- .../Private/FWPSaveDataMigrationContext.cpp | 467 +++++++++++++++++- 3 files changed, 633 insertions(+), 23 deletions(-) diff --git a/Source/FactoryGame/Private/FGObjectReference.cpp b/Source/FactoryGame/Private/FGObjectReference.cpp index ae1827831e..c2bf491511 100644 --- a/Source/FactoryGame/Private/FGObjectReference.cpp +++ b/Source/FactoryGame/Private/FGObjectReference.cpp @@ -21,6 +21,13 @@ void FObjectReferenceDisc::GetRelativePath(const UObject* obj, FString& out_path const ULevel* FObjectReferenceDisc::FindOuterLevel(const UObject* obj){ return nullptr; } const UWorldPartitionRuntimeCell* FObjectReferenceDisc::FindWorldPartitionCell(const UWorld* world, const FString& levelName){ return nullptr; } ULevel* FObjectReferenceDisc::FindLevel(UWorld* world) const{ return nullptr; } +FArchive& operator<<(FArchive& ar, FObjectReferenceDisc& reference) +{ + ar << reference.LevelName; + ar << reference.PathName; + ar << reference.BlueprintRedirectCount; + return ar; +} bool FObjectReferenceDisc::Valid() const{ return bool(); } FString FObjectReferenceDisc::ToString() const{ return FString(); } void FObjectReferenceDisc::ClearRedirects(){ } diff --git a/Source/FactoryGame/Private/FGWorldPartitionRuntimeSpatialHash.cpp b/Source/FactoryGame/Private/FGWorldPartitionRuntimeSpatialHash.cpp index 9adc2467a0..2aa2bdcb28 100644 --- a/Source/FactoryGame/Private/FGWorldPartitionRuntimeSpatialHash.cpp +++ b/Source/FactoryGame/Private/FGWorldPartitionRuntimeSpatialHash.cpp @@ -1,13 +1,177 @@ -// This file has been automatically generated by the Unreal Header Implementation tool +// Copyright Coffee Stain Studios. All Rights Reserved. #include "FGWorldPartitionRuntimeSpatialHash.h" +#include "FGWorldSettings.h" +#include "FactoryGame.h" +#include "WorldPartition/Cook/WorldPartitionCookPackage.h" +#include "WorldPartition/RuntimeSpatialHash/RuntimeSpatialHashGridHelper.h" #if WITH_EDITOR -uint32 UFGWorldPartitionRuntimeSpatialHash::GetGridCellSize(FName GridName){ return uint32(); } -bool UFGWorldPartitionRuntimeSpatialHash::PopulateGeneratorPackageForCook(const TArray& PackagesToCook, TArray& OutModifiedPackage){ return bool(); } -#endif -bool UFGWorldPartitionRuntimeSpatialHash::InjectExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject){ return bool(); } -bool UFGWorldPartitionRuntimeSpatialHash::RemoveExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject){ return bool(); } -UWorldPartitionRuntimeCell* UFGWorldPartitionRuntimeSpatialHash::FindCellByName(FName cellName) const{ return nullptr; } -bool UFGWorldPartitionRuntimeSpatialHash::IsCellContainingWorldLocationLoaded(const FName& GridName, const FVector& Location) const{ return bool(); } -void UFGWorldPartitionRuntimeSpatialHash::RebuildNameToCellMap() const{ } +uint32 UFGWorldPartitionRuntimeSpatialHash::GetGridCellSize( FName GridName ) +{ + uint32 CellSize = 0; + ForEachStreamingGridBreakable( [&CellSize, GridName](const FSpatialHashStreamingGrid& Grid) + { + if(Grid.GridName == GridName) + { + CellSize = Grid.CellSize; + return false; + } + return true; + } ); + return CellSize; +} + +bool UFGWorldPartitionRuntimeSpatialHash::PopulateGeneratorPackageForCook( const TArray& PackagesToCook, + TArray& OutModifiedPackage ) +{ + const auto Result = Super::PopulateGeneratorPackageForCook( PackagesToCook, OutModifiedPackage ); + if(const auto* Level = GetWorld()->PersistentLevel.Get(); Level) + { + const auto* MainWorld = GetWorld(); + AFGWorldSettings* MainWorldSettings = Cast(MainWorld->GetWorldSettings()); + FWPSaveDataMigrationContext::CollectSaveGameValidationDataForPersistentLevel( *Level, *MainWorldSettings ); + ForEachStreamingGrid( [MainWorldSettings](const FSpatialHashStreamingGrid& grid) + { + grid.ForEachRuntimeCell( [MainWorldSettings, &grid](const UWorldPartitionRuntimeCell* Cell)-> bool + { + FWPSaveDataMigrationContext::CollectSaveGameValidationData(grid, *Cast(Cell), *MainWorldSettings); + return true; + } ); + } ); + + } + + return Result; +} + +#endif +bool UFGWorldPartitionRuntimeSpatialHash::InjectExternalStreamingObject( URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject ) +{ + if ( Super::InjectExternalStreamingObject( ExternalStreamingObject ) ) + { + mNameToCellMapDirty = true; + return true; + } + return false; +} + +bool UFGWorldPartitionRuntimeSpatialHash::RemoveExternalStreamingObject( URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject ) +{ + if ( Super::RemoveExternalStreamingObject(ExternalStreamingObject) ) + { + mNameToCellMapDirty = true; + return true; + } + return false; +} + +UWorldPartitionRuntimeCell* UFGWorldPartitionRuntimeSpatialHash::FindCellByName( FName cellName ) const +{ + if ( mNameToCellMapDirty ) + { + RebuildNameToCellMap(); + mNameToCellMapDirty = false; + } + UWorldPartitionRuntimeCell* const* cellMapEntry = mNameToCellMap.Find( cellName ); + return cellMapEntry ? *cellMapEntry : nullptr; +} + +bool UFGWorldPartitionRuntimeSpatialHash::IsCellContainingWorldLocationLoaded( const FName& GridName, const FVector& Location ) const +{ + bool foundLoadedCell = false; + + ForEachStreamingGridBreakable( [ GridName, Location, &foundLoadedCell ]( const FSpatialHashStreamingGrid& grid )-> bool + { + if( grid.GridName == GridName ) + { + FVector2D Location2D = FVector2D( Location ); + const FSquare2DGridHelper& gridHelper = grid.GetGridHelper(); + + //Since we know input location, we can simply calculate the cell coordinates for each grid level. + //Other FSpatialHashStreamingGrid methods use FULL walkthrough against all cells to find the cell containing the location/radius + for( int32 GridLevel = 0; GridLevel < gridHelper.Levels.Num(); ++GridLevel ) + { + FGridCellCoord2 CellCoords; + if( gridHelper.Levels[ GridLevel ].GetCellCoords( Location2D, CellCoords ) ) + { + FGridCellCoord Coords( CellCoords.X, CellCoords.Y, GridLevel ); + + check( grid.GridLevels.IsValidIndex(Coords.Z) ); + + const int64 CoordKey = Coords.Y * gridHelper.Levels[ Coords.Z ].GridSize + Coords.X; + + if( const int32* LayerCellIndexPtr = grid.GridLevels[ Coords.Z ].LayerCellsMapping.Find( CoordKey ) ) + { + const auto& runtimeCells = grid.GridLevels[ Coords.Z ].LayerCells[ *LayerCellIndexPtr ].GridCells; + for( const UWorldPartitionRuntimeCell* OutCell: runtimeCells ) + { + // Check if the X and Y components of the location are within the cell bounds + if( OutCell->GetStreamingStatus() == EStreamingStatus::LEVEL_Visible ) + { + foundLoadedCell = true; + return false; + } + } + } + } + } + + //okay, we didn't find it among the regular cells, now lets check always loaded and injected ones. + //We don't have direct access to them, so just use standard api without spatial query + FWorldPartitionStreamingQuerySource LocationQuery{ Location }; + LocationQuery.bSpatialQuery = false; + TSet< const UWorldPartitionRuntimeCell* > OutCells; + grid.GetCells( LocationQuery, OutCells, false ); + for( const UWorldPartitionRuntimeCell* OutCell: OutCells ) + { + const FBox CellBounds = OutCell->GetCellBounds(); + // Check if the X and Y components of the location are within the cell bounds + if( Location.X >= CellBounds.Min.X && Location.X <= CellBounds.Max.X ) + { + if( Location.Y >= CellBounds.Min.Y && Location.Y <= CellBounds.Max.Y ) + { + if( OutCell->GetStreamingStatus() == EStreamingStatus::LEVEL_Visible ) + { + foundLoadedCell = true; + return false; + } + } + } + } + + return false; + } + + return true; + } ); + + return foundLoadedCell; +} + +void UFGWorldPartitionRuntimeSpatialHash::RebuildNameToCellMap() const +{ + mNameToCellMap.Empty(); + + // We cannot use ForEachStreamingCells because it will not consider cells that are "not relevant" according to the client-only visibility + ForEachStreamingGrid([&]( const FSpatialHashStreamingGrid& streamingGrid ) + { + for ( const FSpatialHashStreamingGridLevel& gridLevel : streamingGrid.GridLevels ) + { + for ( const FSpatialHashStreamingGridLayerCell& layerCell : gridLevel.LayerCells ) + { + for ( UWorldPartitionRuntimeCell* cell : layerCell.GridCells ) + { + // 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), + // but that shouldn't really be the case as external object cells will have their Content Bundle ID encoded in their name + if ( UWorldPartitionRuntimeCell* const* oldCell = mNameToCellMap.Find( cell->GetFName() ) ) + { + fgcheckf( false, TEXT("World Partition Cells with Duplicate Names found: '%s' and '%s'"), *GetFullNameSafe( cell ), *GetFullNameSafe( *oldCell ) ); + } + mNameToCellMap.Add( cell->GetFName(), cell ); + } + } + } + }); + UE_LOG( LogGame, Log, TEXT("Built World Partition cell cache for World '%s', %d cells in cache."), *GetPathNameSafe( GetWorld() ), mNameToCellMap.Num() ); +} diff --git a/Source/FactoryGame/Private/FWPSaveDataMigrationContext.cpp b/Source/FactoryGame/Private/FWPSaveDataMigrationContext.cpp index f9627f4a78..5499b3cbc4 100644 --- a/Source/FactoryGame/Private/FWPSaveDataMigrationContext.cpp +++ b/Source/FactoryGame/Private/FWPSaveDataMigrationContext.cpp @@ -1,19 +1,458 @@ -// This file has been automatically generated by the Unreal Header Implementation tool +// Copyright Coffee Stain Studios. All Rights Reserved. #include "FWPSaveDataMigrationContext.h" +#include "FGArchives64.h" +#include "FGCoreSaveTypes.h" +#include "FGObjectReference.h" +#include "FGSaveInterface.h" +#include "FGSaveSession.h" +#include "FGWorldPartitionRuntimeSpatialHash.h" +#include "FGWorldSettings.h" +#include "FactoryGame.h" +#include "WorldPartition/WorldPartition.h" +#include "WorldPartition/WorldPartitionActorDesc.h" +#include "WorldPartition/WorldPartitionRuntimeCell.h" +#include "WorldPartition/WorldPartitionRuntimeLevelStreamingCell.h" +#include "WorldPartition/WorldPartitionRuntimeSpatialHash.h" -FWPSaveDataMigrationContext::FWPSaveDataMigrationContext(const AFGWorldSettings &InWorldSettings, const FSaveHeader& InSaveHeader): WorldSettings(InWorldSettings), SaveHeader(InSaveHeader){ } +static TAutoConsoleVariable CVarForceFullWPMigration( + TEXT("FG.WorldPartitionMigration.ForceFullMigration"), + false, + TEXT("Whenever to force a full World Partition Migration for the save game data, even if no World Partition has changed"), + ECVF_Default +); #if WITH_EDITOR -void FWPSaveDataMigrationContext::CollectSaveGameValidationData(const FSpatialHashStreamingGrid& Grid, const UWorldPartitionRuntimeLevelStreamingCell& Cell, AFGWorldSettings& WorldSettings){ } -void FWPSaveDataMigrationContext::CollectSaveGameValidationDataForPersistentLevel(const ULevel& Level, AFGWorldSettings& WorldSettings){ } -#endif -FWPSaveDataMigrationContext::~FWPSaveDataMigrationContext(){ } -void FWPSaveDataMigrationContext::MigrateUnpackedData(FUnpackedSaveData& UnpackedSaveData, const bool DataIsFromPersistentLevel){ } -bool FWPSaveDataMigrationContext::ValidateSaveData(FArchive& ReadAr){ return bool(); } -void FWPSaveDataMigrationContext::PerformSaveDataMigration( UFGSaveSession& SaveSession){ } -void FWPSaveDataMigrationContext::SaveValidationData(FArchive& WriteAr, UFGSaveSession& SaveSession){ } -void FWPSaveDataMigrationContext::MigrateBlobs(const TArray &TOCBlob, const TArray &DataBlob, const bool DataIsFromPersistentLevel){ } -void FWPSaveDataMigrationContext::MigrateBlobs(const TArray> &TOCBlob, const TArray> &DataBlo0b, const bool DataIsFromPersistentLevel){ } -void FWPSaveDataMigrationContext::MigrateDestroyedActors(const TArray& DestroyedActors){ } -void FWPSaveDataMigrationContext::RepackSaveData( UFGSaveSession& SaveSession){ } +void FWPSaveDataMigrationContext::CollectSaveGameValidationData( const FSpatialHashStreamingGrid& Grid, const UWorldPartitionRuntimeLevelStreamingCell& Cell, AFGWorldSettings& WorldSettings ) +{ + uint32 CellHash = 0; + // We will need a handle to the world partition instance + UWorldPartition* ActorDescContainer ; + if(!WorldSettings.GetWorldPartition()->IsEmpty() ) + { + ActorDescContainer = WorldSettings.GetWorldPartition(); + } + else + { + const auto *EditorWorld = GEditor->GetEditorWorldContext().World(); + ActorDescContainer = EditorWorld->GetWorldPartition(); + } + // If this is the first time we are encountering this grid, write down the cell size. + auto* pGridValidationData = WorldSettings.SaveGameValidationData.Grids.Find(Grid.GridName); + if( !pGridValidationData ) + { + pGridValidationData = &WorldSettings.SaveGameValidationData.Grids.Add( Grid.GridName ); + pGridValidationData->CellSize = CastChecked(ActorDescContainer->RuntimeHash)->GetGridCellSize( Grid.GridName ); + } + auto &GridValidationData = *pGridValidationData; + + // Ultimately go through all the packages and write them down in the cell mapping + for(const FWorldPartitionRuntimeCellObjectMapping& Package: Cell.GetPackages()) + { + const FSoftObjectPath ObjectPath( Package.Path.ToString() ); + if(const FWorldPartitionActorDesc* ActorDesc = ActorDescContainer->GetActorDesc( Package.ActorInstanceGuid ); ensure(ActorDesc)) + { + // Only keep track of save game actors to reduce the likelihood of having to rebuild save data + const auto ActorClassPath = ActorDesc->GetBaseClass(); + const auto ActorNativeClassPath = ActorDesc->GetNativeClass(); + const auto* ActorClass = FindObject( ActorClassPath ); + const auto* ActorNativeClass = FindObject( ActorNativeClassPath ); + if(ensure(ActorClass || ActorNativeClass)) + { + if( (ActorNativeClass && ActorNativeClass->ImplementsInterface( UFGSaveInterface::StaticClass() )) || + (ActorClass && ActorClass->ImplementsInterface( UFGSaveInterface::StaticClass() ))) + { + CellHash = HashCombine( CellHash, GetTypeHash( ObjectPath.GetSubPathString() ) ); + WorldSettings.WPActorCellMapping.Add( ObjectPath.GetSubPathString() ) = { Grid.GridName, Cell.GetFName() }; + } + } + } + } + + if( CellHash != 0 ) + { + GridValidationData.CellHashes.Add( Cell.GetFName(), CellHash ); + GridValidationData.GridHash = HashCombine( GridValidationData.GridHash, CellHash ); + } +} + +void FWPSaveDataMigrationContext::CollectSaveGameValidationDataForPersistentLevel( const ULevel& Level, AFGWorldSettings& WorldSettings ) +{ + uint32 CellHash = 0; + auto &GridValidationData = WorldSettings.SaveGameValidationData.Grids.FindOrAdd(NAME_None); + + for(const auto Actor: Level.Actors) + { + // TODO we can get null actors for some reason, but they definitely dont implement the save interface :D + if( Actor && Actor->Implements()) + { + const FSoftObjectPath ObjectPath(Actor); + CellHash = HashCombine( CellHash, GetTypeHash( ObjectPath.GetSubPathString() ) ); + WorldSettings.WPActorCellMapping.Add( ObjectPath.GetSubPathString() ) = { NAME_None, NAME_None }; + } + } + if( CellHash != 0 ) + { + GridValidationData.CellHashes.Add( NAME_None, CellHash ); + GridValidationData.GridHash = HashCombine( GridValidationData.GridHash, CellHash ); + } +} + +#endif + +FWPSaveDataMigrationContext::~FWPSaveDataMigrationContext() = default; + +FWPSaveDataMigrationContext::FWPSaveDataMigrationContext( const AFGWorldSettings &InWorldSettings, const struct FSaveHeader& InSaveHeader ) + : WorldSettings( InWorldSettings ) + , SaveHeader( InSaveHeader ) +{} + +void FWPSaveDataMigrationContext::MigrateBlobs( const TArray& TOCBlob, const TArray& DataBlob, + const bool DataIsFromPersistentLevel ) +{ + FUnpackedSaveData UnpackedData; + + if( SaveHeader.SaveVersion < FSaveCustomVersion::SwitchTo64BitSaveArchive ) + { + + FMemoryReader memArchiveTOC(TOCBlob, true); + FMemoryReader memArchiveData(DataBlob, true); + + memArchiveTOC.SetIsLoading( true ); + memArchiveData.SetIsLoading( true ); + + memArchiveTOC.SetCustomVersion( FSaveCustomVersion::GUID, SaveHeader.SaveVersion, SaveSystemConstants::CustomVersionFriendlyName ); + memArchiveData.SetCustomVersion( FSaveCustomVersion::GUID, SaveHeader.SaveVersion, SaveSystemConstants::CustomVersionFriendlyName ); + + memArchiveTOC << UnpackedData.Headers; + UnpackedData.SaveData.Reserve( UnpackedData.Headers.Num() ); + memArchiveData << UnpackedData.SaveData; + + fgcheck( UnpackedData.Headers.Num() == UnpackedData.SaveData.Num() ); + MigrateUnpackedData( UnpackedData, DataIsFromPersistentLevel ); + } +} + +void FWPSaveDataMigrationContext::MigrateBlobs( const TArray>& TOCBlob, const TArray>& DataBlob, const bool DataIsFromPersistentLevel ) +{ + FUnpackedSaveData UnpackedData; + + FFGMemoryReader64 memArchiveTOC(TOCBlob, true); + FFGMemoryReader64 memArchiveData(DataBlob, true); + + memArchiveTOC.SetIsLoading( true ); + memArchiveData.SetIsLoading( true ); + + memArchiveTOC.SetCustomVersion( FSaveCustomVersion::GUID, SaveHeader.SaveVersion, SaveSystemConstants::CustomVersionFriendlyName ); + memArchiveData.SetCustomVersion( FSaveCustomVersion::GUID, SaveHeader.SaveVersion, SaveSystemConstants::CustomVersionFriendlyName ); + + memArchiveTOC << UnpackedData.Headers; + UnpackedData.SaveData.Reserve( UnpackedData.Headers.Num() ); + memArchiveData << UnpackedData.SaveData; + + fgcheck( UnpackedData.Headers.Num() == UnpackedData.SaveData.Num() ); + MigrateUnpackedData( UnpackedData, DataIsFromPersistentLevel ); +} + +void FWPSaveDataMigrationContext::MigrateDestroyedActors( const TArray& DestroyedActors ) +{ + for( const FObjectReferenceDisc& originalObjRef: DestroyedActors ) + { + // WPActorCellMapping contains actor names in the form of PersistentLevel.ActorName + // So to match the format used by it, we need to prefix the actor name with a PersistentLevel + FString ActorSubPathString; + ActorSubPathString.Append(TEXT("PersistentLevel.")); + ActorSubPathString.Append(originalObjRef.GetTopLevelActorName()); + + if( auto* WpCellInfo = WorldSettings.WPActorCellMapping.Find( ActorSubPathString ); WpCellInfo ) + { + auto& MigratedData = PerLevelUnpackedSaveData.FindOrAdd( WpCellInfo->CellName ); + MigratedData.DestroyedActors.Add( UFGSaveSession::FixupObjectReferenceForPartitionedWorld( originalObjRef, WorldSettings ) ); + // The cell that we moved the actor into is a part of invalidated cells now + InvalidatedCells.Add( WpCellInfo->CellName ); + } + else + { + // This object reference does not match any in the new world. + UE_LOG(LogSave, Log, TEXT("Actor %s was removed from world in save file but does not exist in the world currently"), *originalObjRef.ToString()); + } + } + +} + +void FWPSaveDataMigrationContext::MigrateUnpackedData( FUnpackedSaveData& UnpackedSaveData, const bool DataIsFromPersistentLevel ) +{ + for( auto& ObjSaveData: UnpackedSaveData.SaveData ) + { + ObjSaveData.ShouldMigrateObjectRefsToPersistent = true; + ObjSaveData.SaveVersion = SaveHeader.SaveVersion; + } + + for( int32 Ix = 0; Ix < UnpackedSaveData.Headers.Num(); ++Ix ) + { + auto& ObjectSaveHeader = UnpackedSaveData.Headers[Ix]; + + const UClass* Class = ObjectSaveHeader.ResolveClass(); + if( !Class ) + { + // we cannot resolve the class of this object so we'll dismiss it. + UE_LOG( LogSave, Log, TEXT("Cannot resolve class name '%s'"), *ObjectSaveHeader.GetBaseHeader().ClassName); + continue; + } + if ( !DataIsFromPersistentLevel && Class->IsChildOf( AFGWorldSettings::StaticClass() ) ) + { + // There's just no sane way of maintaining both backward and forward compatibility for these so just throw them out. + continue; + } + + if( !ObjectSaveHeader.IsActor() ) + { + FObjectReferenceDisc outerRef; + outerRef.LevelName = ObjectSaveHeader.GetBaseHeader().Reference.LevelName; + outerRef.PathName = ObjectSaveHeader.GetObjectHeader().OuterPathName; + ObjectSaveHeader.GetObjectHeader().OuterPathName = UFGSaveSession::FixupObjectReferenceForPartitionedWorld( outerRef, WorldSettings ).PathName; + } + ObjectSaveHeader.GetBaseHeader().Reference = UFGSaveSession::FixupObjectReferenceForPartitionedWorld( ObjectSaveHeader.GetBaseHeader().Reference, WorldSettings ); + + // WPActorCellMapping contains actor names in the form of PersistentLevel.ActorName + // So to match the format used by it, we need to prefix the actor name with a PersistentLevel + FString ActorSubPathString; + ActorSubPathString.Append(TEXT("PersistentLevel.")); + ActorSubPathString.Append(ObjectSaveHeader.GetBaseHeader().Reference.GetTopLevelActorName()); + + if( auto* wpCellInfo = WorldSettings.WPActorCellMapping.Find( ActorSubPathString ); wpCellInfo ) + { + auto& migratedData = PerLevelUnpackedSaveData.FindOrAdd( wpCellInfo->CellName ); + migratedData.Headers.Emplace( MoveTemp(ObjectSaveHeader) ); + migratedData.SaveData.Emplace( MoveTemp( UnpackedSaveData.SaveData[Ix] ) ); + } + else + { + if( ObjectSaveHeader.IsActor() && ObjectSaveHeader.WasPlacedInLevel() ) + { + // If it was placed in level and isn't in the cell mapping then it must have been removed + } + else + { + // not an actor or just wasn't placed in the level so treat it as runtime spawned actor + auto& migratedData = PerLevelUnpackedSaveData.FindOrAdd( NAME_None ); + migratedData.Headers.Emplace( MoveTemp(ObjectSaveHeader) ); + migratedData.SaveData.Emplace( MoveTemp( UnpackedSaveData.SaveData[Ix] ) ); + } + } + } + MigrateDestroyedActors( std::exchange( UnpackedSaveData.DestroyedActors, {} ) ); +} + +bool FWPSaveDataMigrationContext::ValidateSaveData( FArchive& ReadAr ) +{ + const auto* World = WorldSettings.GetWorld(); + if( SaveHeader.SaveVersion >= FSaveCustomVersion::IntroducedWorldPartition ) + { + if( SaveHeader.IsPartitionedWorld ) + { + if( !World->IsPartitionedWorld() ) + { + UE_LOG( LogSave, Error, TEXT("Loading a save game that was created for a partitioned world into a non-partitioned world is not possibe.") ); + return false; + } + } + + FWorldPartitionValidationData SavedValidationData; + ReadAr << SavedValidationData; + + if( World->IsPartitionedWorld() ) + { + for(const auto& [GridName, GridInfo]: SavedValidationData.Grids) + { + if(const auto* NewGridInfo = WorldSettings.SaveGameValidationData.Grids.Find( GridName ); NewGridInfo) + { + if( GridInfo.CellSize != NewGridInfo->CellSize ) + { + // If the cell size has been changed then none of the existing save data will match any more + for(const auto &[CellName, CellHash]: GridInfo.CellHashes) + { + InvalidatedCells.Add(CellName); + } + } + else if ( GridInfo.GridHash != NewGridInfo->GridHash ) + { + // If the grid hash doesn't match any more then we have cells with new or removed content. + // We need to go through all of the cells and invalidate those that have been changed + for( const auto& [CellName, CellHash]: GridInfo.CellHashes ) + { + if( const auto* NewCellHash = NewGridInfo->CellHashes.Find( CellName ); !NewCellHash || *NewCellHash != CellHash ) + { + InvalidatedCells.Emplace( CellName ); + } + } + } + } + else + { + // This grid doesn't even exist any more so all save data on this grid should be considered invalid + for(const auto &[CellName, CellHash]: GridInfo.CellHashes) + { + InvalidatedCells.Add(CellName); + } + } + } + // Due to buggy behavior old saves may have been correctly flagged as partitioned but they may be lacking any grid validation data + // If that's the case, just treat all the save data as invalid. + if( SavedValidationData.Grids.IsEmpty() ) + { + bIsFullMigration = true; + } + } + } + else + { + // All save data is invalidated and needs conversion only as long as the world is partitioned + bIsFullMigration = World->IsPartitionedWorld(); + } + + // Force a full World Partition migration if we were asked to using CVar + if (CVarForceFullWPMigration.GetValueOnGameThread()) + { + InvalidatedCells.Empty(); + bIsFullMigration = true; + } + return true; +} + +void FWPSaveDataMigrationContext::SaveValidationData( FArchive& WriteAr, UFGSaveSession& SaveSession ) +{ + auto* WorldSettings = Cast< AFGWorldSettings >( SaveSession.GetWorld()->GetWorldSettings() ); + fgcheck(WorldSettings); + WriteAr << WorldSettings->SaveGameValidationData; +} + +void FWPSaveDataMigrationContext::RepackSaveData( class UFGSaveSession& SaveSession ) +{ + SaveSession.mSaveHeader.SaveVersion = FSaveCustomVersion::LatestVersion; + FPackedSaveDataMap PackedSaveDataMap; + + for( auto& [LevelName, LevelSaveData]: PerLevelUnpackedSaveData ) + { + auto& PackedLevelSaveData = PackedSaveDataMap.FindOrAdd( LevelName.ToString() ); + PackedLevelSaveData = MakeUnique( FSaveCustomVersion::LatestVersion ); + + FMemoryWriter64 TOCWriter(PackedLevelSaveData->TOCBlob64); + FMemoryWriter64 DataWriter(PackedLevelSaveData->DataBlob64); + + TOCWriter.SetIsSaving(true); + DataWriter.SetIsSaving(true); + + TOCWriter.SetCustomVersion(FSaveCustomVersion::GUID, FSaveCustomVersion::LatestVersion, SaveSystemConstants::CustomVersionFriendlyName); + DataWriter.SetCustomVersion(FSaveCustomVersion::GUID, FSaveCustomVersion::LatestVersion, SaveSystemConstants::CustomVersionFriendlyName); + + TOCWriter << LevelSaveData.Headers; + DataWriter << LevelSaveData.SaveData; + + PackedLevelSaveData->DestroyedActors = LevelSaveData.DestroyedActors; + } + + if(bIsFullMigration || InvalidatedCells.Contains(NAME_None)) + { + TUniquePtr PersistentSaveData; + PackedSaveDataMap.RemoveAndCopyValue( FName(NAME_None).ToString(), PersistentSaveData ); + if(ensure(PersistentSaveData)) + { + SaveSession.mPersistentAndRuntimeData.TOCBlob64 = MoveTemp( PersistentSaveData->TOCBlob64 ); + SaveSession.mPersistentAndRuntimeData.DataBlob64 = MoveTemp( PersistentSaveData->DataBlob64 ); + } + } + + if(bIsFullMigration) + { + SaveSession.mPerLevelDataMap = MoveTemp( PackedSaveDataMap ); + } + else + { + TSet DeletedCells; + TSet ExistingCells; + for(auto& [CellName, CellSaveData]: SaveSession.mPerLevelDataMap) + { + if( !InvalidatedCells.Contains( FName(CellName) )) + { + continue; + } + + const FName CellFName( *CellName ); + if(TUniquePtr* NewSaveData = PackedSaveDataMap.Find(CellName); NewSaveData) + { + CellSaveData = MoveTemp( *NewSaveData ); + ExistingCells.Add( CellFName ); + } + else + { + DeletedCells.Add( CellFName ); + } + } + + // Delete the cells that are no longer present in the world partition + for( const FName& DeletedCell : DeletedCells) + { + SaveSession.mPerLevelDataMap.Remove( DeletedCell.ToString() ); + } + + // Append new cells to the save system + for ( auto& [CellName, CellSaveData] : PackedSaveDataMap ) + { + if ( !ExistingCells.Contains( *CellName ) ) + { + SaveSession.mPerLevelDataMap.Add( CellName, MoveTemp( CellSaveData ) ); + } + } + } +} + +void FWPSaveDataMigrationContext::PerformSaveDataMigration(UFGSaveSession& SaveSession) +{ + if( !bIsFullMigration && InvalidatedCells.IsEmpty() ) + { + return; + } + + // Migrate unresolved world save data before unpacking cells because this can result in extra cells being unpacked because actors were moved to them + if(!SaveSession.mUnresolvedWorldSaveData.DestroyedActors.IsEmpty()) + { + MigrateDestroyedActors( std::exchange( SaveSession.mUnresolvedWorldSaveData.DestroyedActors, {} ) ); + } + + for(const auto& [LevelName, LevelData]: SaveSession.mPerLevelDataMap) + { + if(bIsFullMigration || InvalidatedCells.Contains(FName(LevelName))) + { + if( SaveHeader.SaveVersion < FSaveCustomVersion::SwitchTo64BitSaveArchive ) + { + MigrateBlobs( LevelData->TOCBlob, LevelData->DataBlob, false ); + MigrateDestroyedActors( LevelData->DestroyedActors ); + } + else + { + MigrateBlobs( LevelData->TOCBlob64, LevelData->DataBlob64, false ); + MigrateDestroyedActors( LevelData->DestroyedActors ); + } + } + } + + if(bIsFullMigration || InvalidatedCells.Contains(NAME_None)) + { + if( SaveHeader.SaveVersion < FSaveCustomVersion::SwitchTo64BitSaveArchive ) + { + MigrateBlobs( SaveSession.mPersistentAndRuntimeData.TOCBlob, SaveSession.mPersistentAndRuntimeData.DataBlob , true ); + } + else + { + MigrateBlobs( SaveSession.mPersistentAndRuntimeData.TOCBlob64, SaveSession.mPersistentAndRuntimeData.DataBlob64 , true ); + } + MigrateDestroyedActors( SaveSession.mPersistentAndRuntimeData.DestroyedActors ); + for( auto& [levelName, destroyedActorArray]: SaveSession.mPersistentAndRuntimeData.LevelToDestroyedActorsMap ) + { + MigrateDestroyedActors( destroyedActorArray ); + } + } + + RepackSaveData( SaveSession ); +}