Exemplo n.º 1
0
void FChunkWriter::FQueuedChunkWriter::GetChunkFilesizes(TMap<FGuid, int64>& OutChunkFileSizes)
{
	FScopeLock ScopeLock(&ChunkFileSizesCS);
	OutChunkFileSizes.Empty(ChunkFileSizes.Num());
	OutChunkFileSizes.Append(ChunkFileSizes);
}
Exemplo n.º 2
0
int32 SGraphPanel::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
#if SLATE_HD_STATS
	SCOPE_CYCLE_COUNTER( STAT_SlateOnPaint_SGraphPanel );
#endif

	CachedAllottedGeometryScaledSize = AllottedGeometry.Size * AllottedGeometry.Scale;

	//Style used for objects that are the same between revisions
	FWidgetStyle FadedStyle = InWidgetStyle;
	FadedStyle.BlendColorAndOpacityTint(FLinearColor(0.45f,0.45f,0.45f,0.45f));

	// First paint the background
	const UEditorExperimentalSettings& Options = *GetDefault<UEditorExperimentalSettings>();

	const FSlateBrush* BackgroundImage = FEditorStyle::GetBrush(TEXT("Graph.Panel.SolidBackground"));
	PaintBackgroundAsLines(BackgroundImage, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId);

	const float ZoomFactor = AllottedGeometry.Scale * GetZoomAmount();

	FArrangedChildren ArrangedChildren(EVisibility::Visible);
	ArrangeChildNodes(AllottedGeometry, ArrangedChildren);

	// Determine some 'global' settings based on current LOD
	const bool bDrawShadowsThisFrame = GetCurrentLOD() > EGraphRenderingLOD::LowestDetail;

	// Because we paint multiple children, we must track the maximum layer id that they produced in case one of our parents
	// wants to an overlay for all of its contents.

	// Save LayerId for comment boxes to ensure they always appear below nodes & wires
	const int32 CommentNodeShadowLayerId = LayerId++;
	const int32 CommentNodeLayerId = LayerId++;

	// Save a LayerId for wires, which appear below nodes but above comments
	// We will draw them later, along with the arrows which appear above nodes.
	const int32 WireLayerId = LayerId++;

	const int32 NodeShadowsLayerId = LayerId;
	const int32 NodeLayerId = NodeShadowsLayerId + 1;
	int32 MaxLayerId = NodeLayerId;

	const FVector2D NodeShadowSize = GetDefault<UGraphEditorSettings>()->GetShadowDeltaSize();
	const UEdGraphSchema* Schema = GraphObj->GetSchema();

	// Draw the child nodes
	{
		// When drawing a marquee, need a preview of what the selection will be.
		const FGraphPanelSelectionSet* SelectionToVisualize = &(SelectionManager.SelectedNodes);
		FGraphPanelSelectionSet SelectionPreview;
		if ( Marquee.IsValid() )
		{			
			ApplyMarqueeSelection(Marquee, SelectionManager.SelectedNodes, SelectionPreview);
			SelectionToVisualize = &SelectionPreview;
		}

		// Context for rendering node infos
		FKismetNodeInfoContext Context(GraphObj);

		TArray<FGraphDiffControl::FNodeMatch> NodeMatches;
		for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex)
		{
			FArrangedWidget& CurWidget = ArrangedChildren[ChildIndex];
			TSharedRef<SGraphNode> ChildNode = StaticCastSharedRef<SGraphNode>(CurWidget.Widget);
			
			// Examine node to see what layers we should be drawing in
			int32 ShadowLayerId = NodeShadowsLayerId;
			int32 ChildLayerId = NodeLayerId;

			// If a comment node, draw in the dedicated comment slots
			{
				UObject* NodeObj = ChildNode->GetObjectBeingDisplayed();
				if (NodeObj && NodeObj->IsA(UEdGraphNode_Comment::StaticClass()))
				{
					ShadowLayerId = CommentNodeShadowLayerId;
					ChildLayerId = CommentNodeLayerId;
				}
			}


			const bool bNodeIsVisible = FSlateRect::DoRectanglesIntersect( CurWidget.Geometry.GetClippingRect(), MyClippingRect );

			if (bNodeIsVisible)
			{
				const bool bSelected = SelectionToVisualize->Contains( StaticCastSharedRef<SNodePanel::SNode>(CurWidget.Widget)->GetObjectBeingDisplayed() );

				// Handle Node renaming once the node is visible
				if( bSelected && ChildNode->IsRenamePending() )
				{
					ChildNode->ApplyRename();
				}

				// Draw the node's shadow.
				if (bDrawShadowsThisFrame || bSelected)
				{
					const FSlateBrush* ShadowBrush = ChildNode->GetShadowBrush(bSelected);
					FSlateDrawElement::MakeBox(
						OutDrawElements,
						ShadowLayerId,
						CurWidget.Geometry.ToInflatedPaintGeometry(NodeShadowSize),
						ShadowBrush,
						MyClippingRect
						);
				}

				// Draw the comments and information popups for this node, if it has any.
				{
					const SNodePanel::SNode::FNodeSlot& CommentSlot = ChildNode->GetOrAddSlot( ENodeZone::TopCenter );
					float CommentBubbleY = -CommentSlot.Offset.Get().Y;
					Context.bSelected = bSelected;
					TArray<FGraphInformationPopupInfo> Popups;

					{
						ChildNode->GetNodeInfoPopups(&Context, /*out*/ Popups);
					}

					for (int32 PopupIndex = 0; PopupIndex < Popups.Num(); ++PopupIndex)
					{
						FGraphInformationPopupInfo& Popup = Popups[PopupIndex];
						PaintComment(Popup.Message, CurWidget.Geometry, MyClippingRect, OutDrawElements, ChildLayerId, Popup.BackgroundColor, /*inout*/ CommentBubbleY, InWidgetStyle);
					}
				}

				int32 CurWidgetsMaxLayerId;
				{
					UEdGraphNode* NodeObj = Cast<UEdGraphNode>(ChildNode->GetObjectBeingDisplayed());

					/** When diffing nodes, nodes that are different between revisions are opaque, nodes that have not changed are faded */
					FGraphDiffControl::FNodeMatch NodeMatch = FGraphDiffControl::FindNodeMatch(GraphObjToDiff, NodeObj, NodeMatches);
					if (NodeMatch.IsValid())
					{
						NodeMatches.Add(NodeMatch);
					}
					const bool bNodeIsDifferent = (!GraphObjToDiff || NodeMatch.Diff());

					/* When dragging off a pin, we want to duck the alpha of some nodes */
					TSharedPtr< SGraphPin > OnlyStartPin = (1 == PreviewConnectorFromPins.Num()) ? PreviewConnectorFromPins[0].FindInGraphPanel(*this) : TSharedPtr< SGraphPin >();
					const bool bNodeIsNotUsableInCurrentContext = Schema->FadeNodeWhenDraggingOffPin(NodeObj, OnlyStartPin.IsValid() ? OnlyStartPin.Get()->GetPinObj() : NULL);
					const FWidgetStyle& NodeStyleToUse = (bNodeIsDifferent && !bNodeIsNotUsableInCurrentContext)? InWidgetStyle : FadedStyle;

					// Draw the node.O
					CurWidgetsMaxLayerId = CurWidget.Widget->Paint( Args.WithNewParent(this), CurWidget.Geometry, MyClippingRect, OutDrawElements, ChildLayerId, NodeStyleToUse, ShouldBeEnabled( bParentEnabled ) );
				}

				// Draw the node's overlay, if it has one.
				{
					// Get its size
					const FVector2D WidgetSize = CurWidget.Geometry.Size;

					{
						TArray<FOverlayBrushInfo> OverlayBrushes;
						ChildNode->GetOverlayBrushes(bSelected, WidgetSize, /*out*/ OverlayBrushes);

						for (int32 BrushIndex = 0; BrushIndex < OverlayBrushes.Num(); ++BrushIndex)
						{
							FOverlayBrushInfo& OverlayInfo = OverlayBrushes[BrushIndex];
							const FSlateBrush* OverlayBrush = OverlayInfo.Brush;
							if(OverlayBrush != NULL)
							{
								FPaintGeometry BouncedGeometry = CurWidget.Geometry.ToPaintGeometry(OverlayInfo.OverlayOffset, OverlayBrush->ImageSize, 1.f);

								// Handle bouncing
								const float BounceValue = FMath::Sin(2.0f * PI * BounceCurve.GetLerpLooping());
								BouncedGeometry.DrawPosition += (OverlayInfo.AnimationEnvelope * BounceValue * ZoomFactor);

								CurWidgetsMaxLayerId++;
								FSlateDrawElement::MakeBox(
									OutDrawElements,
									CurWidgetsMaxLayerId,
									BouncedGeometry,
									OverlayBrush,
									MyClippingRect
									);
							}

						}
					}

					{
						TArray<FOverlayWidgetInfo> OverlayWidgets = ChildNode->GetOverlayWidgets(bSelected, WidgetSize);

						for (int32 WidgetIndex = 0; WidgetIndex < OverlayWidgets.Num(); ++WidgetIndex)
						{
							FOverlayWidgetInfo& OverlayInfo = OverlayWidgets[WidgetIndex];
							if(OverlayInfo.Widget->GetVisibility() == EVisibility::Visible)
							{
								// call SlatePrepass as these widgets are not in the 'normal' child hierarchy
								OverlayInfo.Widget->SlatePrepass();

								const FGeometry WidgetGeometry = CurWidget.Geometry.MakeChild(OverlayInfo.OverlayOffset, OverlayInfo.Widget->GetDesiredSize(), 1.f);

								OverlayInfo.Widget->Paint(Args.WithNewParent(this), WidgetGeometry, MyClippingRect, OutDrawElements, CurWidgetsMaxLayerId, InWidgetStyle, bParentEnabled);
							}
						}
					}
				}

				MaxLayerId = FMath::Max( MaxLayerId, CurWidgetsMaxLayerId + 1 );
			}
		}
	}

	MaxLayerId += 1;


	// Draw connections between pins 
	if (Children.Num() > 0 )
	{

		//@TODO: Pull this into a factory like the pin and node ones
		FConnectionDrawingPolicy* ConnectionDrawingPolicy;
		{
			ConnectionDrawingPolicy = Schema->CreateConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
			if (!ConnectionDrawingPolicy)
			{
				if (Schema->IsA(UAnimationGraphSchema::StaticClass()))
				{
					ConnectionDrawingPolicy = new FAnimGraphConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
				}
				else if (Schema->IsA(UAnimationStateMachineSchema::StaticClass()))
				{
					ConnectionDrawingPolicy = new FStateMachineConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
				}
				else if (Schema->IsA(UEdGraphSchema_K2::StaticClass()))
				{
					ConnectionDrawingPolicy = new FKismetConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
				}
				else if (Schema->IsA(USoundCueGraphSchema::StaticClass()))
				{
					ConnectionDrawingPolicy = new FSoundCueGraphConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
				}
				else if (Schema->IsA(UMaterialGraphSchema::StaticClass()))
				{
					ConnectionDrawingPolicy = new FMaterialGraphConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements, GraphObj);
				}
				else
				{
					ConnectionDrawingPolicy = new FConnectionDrawingPolicy(WireLayerId, MaxLayerId, ZoomFactor, MyClippingRect, OutDrawElements);
				}
			}
		}

		TArray<TSharedPtr<SGraphPin>> OverridePins;
		for (const FGraphPinHandle& Handle : PreviewConnectorFromPins)
		{
			TSharedPtr<SGraphPin> Pin = Handle.FindInGraphPanel(*this);
			if (Pin.IsValid())
			{
				OverridePins.Add(Pin);
			}
		}
		ConnectionDrawingPolicy->SetHoveredPins(CurrentHoveredPins, OverridePins, TimeSinceMouseEnteredPin);
		ConnectionDrawingPolicy->SetMarkedPin(MarkedPin);

		// Get the set of pins for all children and synthesize geometry for culled out pins so lines can be drawn to them.
		TMap<TSharedRef<SWidget>, FArrangedWidget> PinGeometries;
		TSet< TSharedRef<SWidget> > VisiblePins;
		for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex)
		{
			TSharedRef<SGraphNode> ChildNode = StaticCastSharedRef<SGraphNode>(Children[ChildIndex]);

			// If this is a culled node, approximate the pin geometry to the corner of the node it is within
			if (IsNodeCulled(ChildNode, AllottedGeometry))
			{
				TArray< TSharedRef<SWidget> > NodePins;
				ChildNode->GetPins(NodePins);

				const FVector2D NodeLoc = ChildNode->GetPosition();
				const FGeometry SynthesizedNodeGeometry(GraphCoordToPanelCoord(NodeLoc), AllottedGeometry.AbsolutePosition, FVector2D::ZeroVector, 1.f);

				for (TArray< TSharedRef<SWidget> >::TConstIterator NodePinIterator(NodePins); NodePinIterator; ++NodePinIterator)
				{
					const SGraphPin& PinWidget = static_cast<const SGraphPin&>((*NodePinIterator).Get());
					FVector2D PinLoc = NodeLoc + PinWidget.GetNodeOffset();

					const FGeometry SynthesizedPinGeometry(GraphCoordToPanelCoord(PinLoc), AllottedGeometry.AbsolutePosition, FVector2D::ZeroVector, 1.f);
					PinGeometries.Add(*NodePinIterator, FArrangedWidget(*NodePinIterator, SynthesizedPinGeometry));
				}

				// Also add synthesized geometries for culled nodes
				ArrangedChildren.AddWidget( FArrangedWidget(ChildNode, SynthesizedNodeGeometry) );
			}
			else
			{
				ChildNode->GetPins(VisiblePins);
			}
		}

		// Now get the pin geometry for all visible children and append it to the PinGeometries map
		TMap<TSharedRef<SWidget>, FArrangedWidget> VisiblePinGeometries;
		{
			this->FindChildGeometries(AllottedGeometry, VisiblePins, VisiblePinGeometries);
			PinGeometries.Append(VisiblePinGeometries);
		}

		// Draw preview connections (only connected on one end)
		if (PreviewConnectorFromPins.Num() > 0)
		{
			for (const FGraphPinHandle& Handle : PreviewConnectorFromPins)
			{
				TSharedPtr< SGraphPin > CurrentStartPin = Handle.FindInGraphPanel(*this);
				if (!CurrentStartPin.IsValid())
				{
					continue;
				}
				const FArrangedWidget* PinGeometry = PinGeometries.Find( CurrentStartPin.ToSharedRef() );

				if (PinGeometry != NULL)
				{
					FVector2D StartPoint;
					FVector2D EndPoint;

					if (CurrentStartPin->GetDirection() == EGPD_Input)
					{
						StartPoint = AllottedGeometry.AbsolutePosition + PreviewConnectorEndpoint;
						EndPoint = FGeometryHelper::VerticalMiddleLeftOf( PinGeometry->Geometry ) - FVector2D(ConnectionDrawingPolicy->ArrowRadius.X, 0);
					}
					else
					{
						StartPoint = FGeometryHelper::VerticalMiddleRightOf( PinGeometry->Geometry );
						EndPoint = AllottedGeometry.AbsolutePosition + PreviewConnectorEndpoint;
					}

					ConnectionDrawingPolicy->DrawPreviewConnector(PinGeometry->Geometry, StartPoint, EndPoint, CurrentStartPin.Get()->GetPinObj());
				}

				//@TODO: Re-evaluate this incompatible mojo; it's mutating every pin state every frame to accomplish a visual effect
				ConnectionDrawingPolicy->SetIncompatiblePinDrawState(CurrentStartPin, VisiblePins);
			}
		}
		else
		{
			//@TODO: Re-evaluate this incompatible mojo; it's mutating every pin state every frame to accomplish a visual effect
			ConnectionDrawingPolicy->ResetIncompatiblePinDrawState(VisiblePins);
		}

		// Draw all regular connections
		ConnectionDrawingPolicy->Draw(PinGeometries, ArrangedChildren);

		delete ConnectionDrawingPolicy;
	}

	// Draw a shadow overlay around the edges of the graph
	++MaxLayerId;
	PaintSurroundSunkenShadow(FEditorStyle::GetBrush(TEXT("Graph.Shadow")), AllottedGeometry, MyClippingRect, OutDrawElements, MaxLayerId);

	if(ShowGraphStateOverlay.Get())
	{
		const FSlateBrush* BorderBrush = nullptr;
		if((GEditor->bIsSimulatingInEditor || GEditor->PlayWorld != NULL))
		{
			// Draw a surrounding indicator when PIE is active, to make it clear that the graph is read-only, etc...
			BorderBrush = FEditorStyle::GetBrush(TEXT("Graph.PlayInEditor"));
		}
		else if(!IsEditable.Get())
		{
			// Draw a different border when we're not simulating but the graph is read-only
			BorderBrush = FEditorStyle::GetBrush(TEXT("Graph.ReadOnlyBorder"));
		}

		if(BorderBrush)
		{
			// Actually draw the border
			FSlateDrawElement::MakeBox(
				OutDrawElements,
				MaxLayerId,
				AllottedGeometry.ToPaintGeometry(),
				BorderBrush,
				MyClippingRect
				);
		}
	}

	// Draw the marquee selection rectangle
	PaintMarquee(AllottedGeometry, MyClippingRect, OutDrawElements, MaxLayerId);

	// Draw the software cursor
	++MaxLayerId;
	PaintSoftwareCursor(AllottedGeometry, MyClippingRect, OutDrawElements, MaxLayerId);

	return MaxLayerId;
}
Exemplo n.º 3
0
	void FManifestBuilderImpl::BuildManifest()
	{
		TMap<FGuid, FChunkInfo> ChunkInfoLookup;
		bool Running = true;
		while (Running)
		{
			FDataScannerPtr NextScanner = GetNextScanner();
			if (NextScanner.IsValid())
			{
				FDataScanResult ScanResult = NextScanner->GetResultWhenComplete();
				ChunkInfoLookup.Append(ScanResult.ChunkInfo);

				// Always reverse for now
				if (ScanResult.DataStructure.Num() > 0)
				{
					FChunkPart& ChunkPart = ScanResult.DataStructure[0];
					if (ChunkPart.DataOffset != FileBuilder.CurrentDataPos)
					{
						check(ChunkPart.DataOffset < FileBuilder.CurrentDataPos); // Missing data!

						bool FoundPosition = false;
						uint64 DataCount = 0;
						for (int32 FileIdx = 0; FileIdx < Manifest->Data->FileManifestList.Num() && !FoundPosition; ++FileIdx)
						{
							FFileManifestData& FileManifest = Manifest->Data->FileManifestList[FileIdx];
							FileManifest.Init();
							uint64 FileStartIdx = DataCount;
							uint64 FileEndIdx = FileStartIdx + FileManifest.GetFileSize();
							if (FileEndIdx > ChunkPart.DataOffset)
							{
								for (int32 ChunkIdx = 0; ChunkIdx < FileManifest.FileChunkParts.Num() && !FoundPosition; ++ChunkIdx)
								{
									FChunkPartData& ChunkPartData = FileManifest.FileChunkParts[ChunkIdx];
									uint64 ChunkPartEndIdx = DataCount + ChunkPartData.Size;
									if (ChunkPartEndIdx < ChunkPart.DataOffset)
									{
										DataCount += ChunkPartData.Size;
									}
									else if (ChunkPartEndIdx > ChunkPart.DataOffset)
									{
										ChunkPartData.Size = ChunkPart.DataOffset - DataCount;
										FileBuilder.CurrentDataPos = DataCount + ChunkPartData.Size;
										FileManifest.FileChunkParts.SetNum(ChunkIdx + 1, false);
										FileManifest.FileChunkParts.Emplace();
										Manifest->Data->FileManifestList.SetNum(FileIdx + 1, false);
										FileBuilder.FileManifest = &Manifest->Data->FileManifestList.Last();
										bool FoundFile = BuildStreamer->GetFileSpan(FileStartIdx, FileBuilder.FileSpan);
										check(FoundFile); // Incorrect positional tracking
										FoundPosition = true;
									}
									else
									{
										FileBuilder.CurrentDataPos = DataCount + ChunkPartData.Size;
										FileManifest.FileChunkParts.SetNum(ChunkIdx + 1, false);
										FileManifest.FileChunkParts.Emplace();
										Manifest->Data->FileManifestList.SetNum(FileIdx + 1, false);
										FileBuilder.FileManifest = &Manifest->Data->FileManifestList.Last();
										bool FoundFile = BuildStreamer->GetFileSpan(FileStartIdx, FileBuilder.FileSpan);
										check(FoundFile); // Incorrect positional tracking
										FoundPosition = true;
									}
								}
							}
							else if (FileEndIdx < ChunkPart.DataOffset)
							{
								DataCount += FileManifest.GetFileSize();
							}
							else
							{
								FileBuilder.FileManifest = nullptr;
								FileBuilder.CurrentDataPos = DataCount + FileManifest.GetFileSize();
								Manifest->Data->FileManifestList.SetNum(FileIdx + 1, false);
								FoundPosition = true;
							}
						}

						check(ChunkPart.DataOffset == FileBuilder.CurrentDataPos);
						check(FileBuilder.FileManifest == nullptr || FileBuilder.FileSpan.Filename == Manifest->Data->FileManifestList.Last().Filename);
					}
				}

				for (int32 idx = 0; idx < ScanResult.DataStructure.Num(); ++idx)
				{
					FChunkPart& ChunkPart = ScanResult.DataStructure[idx];
					// Starting new file?
					if (FileBuilder.FileManifest == nullptr)
					{
						Manifest->Data->FileManifestList.Emplace();
						FileBuilder.FileManifest = &Manifest->Data->FileManifestList.Last();

						bool FoundFile = BuildStreamer->GetFileSpan(FileBuilder.CurrentDataPos, FileBuilder.FileSpan);
						check(FoundFile); // Incorrect positional tracking

						FileBuilder.FileManifest->Filename = FileBuilder.FileSpan.Filename;
						FileBuilder.FileManifest->FileChunkParts.Emplace();
					}

					FChunkPartData& FileChunkPartData = FileBuilder.FileManifest->FileChunkParts.Last();
					FileChunkPartData.Guid = ChunkPart.ChunkGuid;
					FileChunkPartData.Offset = (FileBuilder.CurrentDataPos - ChunkPart.DataOffset) + ChunkPart.ChunkOffset;

					// Process data into file manifests
					int64 FileDataLeft = (FileBuilder.FileSpan.StartIdx + FileBuilder.FileSpan.Size) - FileBuilder.CurrentDataPos;
					int64 ChunkDataLeft = (ChunkPart.DataOffset + ChunkPart.PartSize) - FileBuilder.CurrentDataPos;
					check(FileDataLeft > 0);
					check(ChunkDataLeft > 0);

					if (ChunkDataLeft >= FileDataLeft)
					{
						FileBuilder.CurrentDataPos += FileDataLeft;
						FileChunkPartData.Size = FileDataLeft;
					}
					else
					{
						FileBuilder.CurrentDataPos += ChunkDataLeft;
						FileChunkPartData.Size = ChunkDataLeft;
					}

					FileDataLeft = (FileBuilder.FileSpan.StartIdx + FileBuilder.FileSpan.Size) - FileBuilder.CurrentDataPos;
					ChunkDataLeft = (ChunkPart.DataOffset + ChunkPart.PartSize) - FileBuilder.CurrentDataPos;
					check(FileDataLeft == 0 || ChunkDataLeft == 0);
					// End of file?
					if (FileDataLeft == 0)
					{
						// Fill out rest of data??
						FFileSpan FileSpan;
						bool FoundFile = BuildStreamer->GetFileSpan(FileBuilder.FileSpan.StartIdx, FileSpan);
						check(FoundFile); // Incorrect positional tracking
						check(FileSpan.Filename == FileBuilder.FileManifest->Filename);
						FMemory::Memcpy(FileBuilder.FileManifest->FileHash.Hash, FileSpan.SHAHash.Hash, FSHA1::DigestSize);
						FFileAttributes Attributes = FileAttributesMap.FindRef(FileSpan.Filename);
						FileBuilder.FileManifest->bIsUnixExecutable = Attributes.bUnixExecutable || FileSpan.IsUnixExecutable;
						FileBuilder.FileManifest->SymlinkTarget = FileSpan.SymlinkTarget;
						FileBuilder.FileManifest->bIsReadOnly = Attributes.bReadOnly;
						FileBuilder.FileManifest->bIsCompressed = Attributes.bCompressed;
						FileBuilder.FileManifest->InstallTags = Attributes.InstallTags.Array();
						FileBuilder.FileManifest->Init();
						check(FileBuilder.FileManifest->GetFileSize() == FileBuilder.FileSpan.Size);
						FileBuilder.FileManifest = nullptr;
					}
					else if (ChunkDataLeft == 0)
					{
						FileBuilder.FileManifest->FileChunkParts.Emplace();
					}

					// Continue with this chunk?
					if (ChunkDataLeft > 0)
					{
						--idx;
					}
				}
			}
			else
			{
				if (EndOfData)
				{
					Running = false;
				}
				else
				{
					CheckForWork->Wait();
					CheckForWork->Reset();
				}
			}
		}

		// Fill out chunk list from only chunks that remain referenced
		TSet<FGuid> ReferencedChunks;
		for (const auto& FileManifest : Manifest->Data->FileManifestList)
		{
			for (const auto& ChunkPart : FileManifest.FileChunkParts)
			{
				if (ReferencedChunks.Contains(ChunkPart.Guid) == false)
				{
					auto& ChunkInfo = ChunkInfoLookup[ChunkPart.Guid];
					ReferencedChunks.Add(ChunkPart.Guid);
					Manifest->Data->ChunkList.Emplace();
					auto& ChunkInfoData = Manifest->Data->ChunkList.Last();
					ChunkInfoData.Guid = ChunkPart.Guid;
					ChunkInfoData.Hash = ChunkInfo.Hash;
					FMemory::Memcpy(ChunkInfoData.ShaHash.Hash, ChunkInfo.ShaHash.Hash, FSHA1::DigestSize);
					ChunkInfoData.FileSize = ChunkInfo.ChunkFileSize;
					ChunkInfoData.GroupNumber = FCrc::MemCrc32(&ChunkPart.Guid, sizeof(FGuid)) % 100;
				}
			}
		}

		// Get empty files
		FSHA1 EmptyHasher;
		EmptyHasher.Final();
		const TArray< FString >& EmptyFileList = BuildStreamer->GetEmptyFiles();
		for (const auto& EmptyFile : EmptyFileList)
		{
			Manifest->Data->FileManifestList.Emplace();
			FFileManifestData& EmptyFileManifest = Manifest->Data->FileManifestList.Last();
			EmptyFileManifest.Filename = EmptyFile;
			EmptyHasher.GetHash(EmptyFileManifest.FileHash.Hash);
		}

		// Fill out lookups
		Manifest->InitLookups();
	}