Files
UnrealEngineUWP/Engine/Source/Editor/GameProjectGeneration/Private/GameProjectUtils.cpp
Marc Audy 57d3748759 Copying //UE4/Dev-Framework to //UE4/Dev-Main (Source: //UE4/Dev-Framework @ 3227619)
#rb none
#lockdown Nick.Penwarden

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3198996 on 2016/11/15 by Marc.Audy

	BeginPlay calls will now be dispatched in a consistent order regardless of placed in persistent level, streamed in level, or dynamically spawned
	AActor::BeginPlay is now protected, you should call DispatchBeginPlay instead.
	#jira UE-21136

Change 3199019 on 2016/11/15 by Marc.Audy

	Mark user-facing BeginPlay calls as protected

Change 3200128 on 2016/11/16 by Thomas.Sarkanen

	Dont propgate threaded update flag from UAnimBluepint to CDO if we fail thread safety checks

	Also fully deprecated (with _DEPRECATED) older flags in UAnimInstance.

	#jira UE-38362 - Disable multi-threaded update when anim blueprints are not thread-safe

Change 3200133 on 2016/11/16 by Martin.Wilson

	Fix Set Anim Instance Class not working on the second attempt (InitAnim would not be called)

	#jira UE-18798

Change 3200167 on 2016/11/16 by Martin.Wilson

	Newly added virtual bones are now selected in the skeleton tree

	#jira UE-37776

Change 3200255 on 2016/11/16 by James.Golding

	Stop SkeletalMeshTypes.h being globally included

Change 3200289 on 2016/11/16 by Jurre.deBaare

	Hidden Material References from Mesh Components Fix
	#fix Make sure that in PostEditChangeProp we reset the override material arrays
	#misc changed a property comparison to use GET_MEMBER_NAME_CHECKED instead
	#jira UE-38108

Change 3200291 on 2016/11/16 by Jurre.deBaare

	Imported Alembic skeletal anims have cut-off shadow due to moving out of the bounds
	#fix retrieve bounds from alembic archive at various levels (global, transform, meshes) and build archive bounds which is set on the animation sequence
	#jira UE-37274

Change 3200293 on 2016/11/16 by Jurre.deBaare

	Overlapping UV's cause merge actor texture baking issues
	#fix Only look for overlapping UVs if vertex data baking is actually expected/enabled
	#jira UE-37220

Change 3200294 on 2016/11/16 by Jurre.deBaare

	Scrubbing Playback Speed under Geometry Cache in the details panel is too sensitive
	#fix Make the UIMin/Max smaller than the clamping value for proper user interaction while sliding (thanks James for the tip!)
	#jira UE-36679

Change 3200295 on 2016/11/16 by Jurre.deBaare

	Merge Actor Specific LOD level can be set to 8
	#fix Change clamping value and added UI clamp metadata
	#jira UE-37134

Change 3200296 on 2016/11/16 by Jurre.deBaare

	In Merge Actors if you select use specific Lod level you have access to all the merge material settings
	#fix Added edit condition to non-grayed out material settings
	#jira UE-36667

Change 3200303 on 2016/11/16 by Thomas.Sarkanen

	Fixed diagonal current scrub value in anim curves

	#jira UE-35787 - The red time indicator for viewing curves in persona is slightly tilted

Change 3200304 on 2016/11/16 by Thomas.Sarkanen

	Rezero is now explicit about what it does (current vs. specified frame)

	Also no longer ingores Z-offset (legacy feature - root motion can have any translation, not just 2D).

	#jira UE-35985 - Rezero doesn't work by frame

Change 3200307 on 2016/11/16 by Thomas.Sarkanen

	Add curve panel to anim BP editor

	Also improve curve modification message routing. We were needlessly passing delegates up and down the widget hierarchy and conflating smart name edits with curve edits (key addition etc.).

	#jira UE-35742 - Anim Curve Viewer allowed in Anim BP

Change 3200313 on 2016/11/16 by Jurre.deBaare

	Animations with materials driven by scalar parameters from curves wont update until persona is closed and reopened
	#fix in debug skeletal mesh component just mark the cached parameters dirty every tick
	#jira UE-35786

Change 3200316 on 2016/11/16 by Jurre.deBaare

	Converted Skeletal To Static Mesh Gets Corrupted When Merged
	#fix Assume that the all static meshes will contain valid texture coordinates for channel 0 (which is expect by static mesh code as well)
	#misc Ensure that we set the lightmap index for converted skeletal meshes to either an empty one or the highest one used
	#jira UE-37988

Change 3200321 on 2016/11/16 by Jurre.deBaare

	Scrolling/scroll bar are disabled in Alembic Import window if you scroll a certain way down
	#fix change the way the layout is constructed
	#jira UE-37260

Change 3200323 on 2016/11/16 by Jurre.deBaare

	Toggling sky in Persona does not effect reflections
	#fix turn of skylight together with the actual environment sphere
	#misc found incorrect copy paste in toggling floor/environment visibility with key stroke
	#jira UE-26796

Change 3200324 on 2016/11/16 by Jurre.deBaare

	Open Merge Actor menu on right clicking two selected actors
	#fix Added option 'Merge Actors' to right-click context menu when having selected one or multiple actors in the viewport
	#jira UE-36892

Change 3200331 on 2016/11/16 by Benn.Gallagher

	Added support for suspending clothing simulations at runtime, exposed also to blueperints. And aded option in Persona to pause simulations when animations are paused.
	#jira UE-38620

Change 3200334 on 2016/11/16 by Jurre.deBaare

	Dynamic light settings in Persona viewport cause edges to appear hardened
	#fix Makeing the directional light stationary to ups the shadowing quality
	#jira UE-37188

Change 3200356 on 2016/11/16 by Jurre.deBaare

	Rate scale option for animation nodes in blend spaces
	#added Rate scale variable to blend space samples, these rates are now multiplied with the global rate scale during playback
	#misc bumped framework object version to update all blendspaces on load
	#jira UE-16207

Change 3200380 on 2016/11/16 by Jurre.deBaare

	Fix for Mac CIS issues

Change 3200383 on 2016/11/16 by Marc.Audy

	Split FAttenuationSettings in to FBaseAttenuationSettings and FSoundAttenuationSettings in preparation for reuse of the base attenuation for force feedback

Change 3200385 on 2016/11/16 by James.Golding

	Refactor SkeletalMesh to use same color buffer type as StaticMesh

Change 3200407 on 2016/11/16 by James.Golding

	Fix CIS error in FbxAutomationTests.cpp

Change 3200417 on 2016/11/16 by Jurre.deBaare

	Fix for CIS issues
	#fix Rogue }

Change 3200446 on 2016/11/16 by Martin.Wilson

	Change fix for Set Anim Instance Class from CL 3200133

	#jira UE-18798

Change 3200579 on 2016/11/16 by Martin.Wilson

	Fix for serialization crash in Odin

	#jir UE-38683

Change 3200659 on 2016/11/16 by Martin.Wilson

	Fix build errors

Change 3200801 on 2016/11/16 by Lina.Halper

	Fix error message

Change 3200873 on 2016/11/16 by Lina.Halper

	Test case for Update Rate Optimization

	- LOD_URO_Map.umap - test map
	- LODPawn - pawn that contains mesh with URO setting
	- You can tweak the value in LODPawn

Change 3201017 on 2016/11/16 by Lina.Halper

	- Allow slave component to be removed when setting master pose to nullptr
	- licensee reported this issue. https://udn.unrealengine.com/questions/321037/skeletalmeshcomponent.html

Change 3201765 on 2016/11/17 by Jurre.deBaare

	Improved tooltip for FBlendParameter.GridNum

Change 3201817 on 2016/11/17 by Thomas.Sarkanen

	Added display/edit of bone transforms in details panel

	Added UBoneProxy tickable editor object held by the skeleton tree that updates its internal transforms in Tick().
	Updated various bits of supporting code to allow selection to be properly preserved in cases such as undo/redo. This allows the bone proxy object to be displayed over an undo/redo event. It also fixes some inconsistency with selection between the skeleton tree and the preview scene.
	Breaking change: Updated FOnPreviewMeshChangedMulticaster delegate signature to take both the old and new skeletal mesh. This is to allow clients to skip certain logic if the skeletal mesh hasnt really changed (in this case de-selection).

	#jira UE-38144 - Selected Bone Transform not visible in Persona on the AnimBP tab

Change 3201819 on 2016/11/17 by Thomas.Sarkanen

	Fix CIS error

Change 3201901 on 2016/11/17 by Lina.Halper

	With new system, the skeleton curve count is not the one we should check but BoneContainer.GetAnimCurveNameUids().
	- removed GetCurveNumber from skeleton
	- changed curve count to  use BoneContainer's curve list.

	#code review: Laurent.Delayen

Change 3201999 on 2016/11/17 by Thomas.Sarkanen

	Add local/world transform editing to bone editing

	Added details customization & support code for world-space editing of bone transforms

	#jira UE-38144 - Selected Bone Transform not visible in Persona on the AnimBP tab

Change 3202111 on 2016/11/17 by mason.seay

	Potential test assets for HLOD

Change 3202240 on 2016/11/17 by Thomas.Sarkanen

	Fixed extra whitespace not being removed in front of console commands.

	GitHub #2843

	#jira UE-37019 - GitHub 2843 : Fixed extra whitespace not being removed in front of console commands.

Change 3202259 on 2016/11/17 by Jurre.deBaare

	Readded missing shadows in advanced preview scene

Change 3203180 on 2016/11/17 by mason.seay

	Moved and updated URO Map

Change 3203678 on 2016/11/18 by Thomas.Sarkanen

	Bug fix for menu extenders in PhAT.

	GitHub #2550
	#jira UE-32678 - GitHub 2550 : Bug fix for menu extenders in PhAT.

Change 3203679 on 2016/11/18 by Thomas.Sarkanen

	Fixed LOD hysteresis not being properly converted from the old metric

	This addreses some 'LOD lag' issues seen when just treating as an equivalent fudge factor, as the magnitude needed to have an effect has changed.

	#jira UE-38640 - Skeletal mesh LODs render incorrectly and incosistently

Change 3203747 on 2016/11/18 by Jurre.deBaare

	Crash when repeatedly undoing and readding of animation to a AnimOffset 1D - IsValidBlendSampleIndex
	#fix Ensure we reset the hightlighting / dragging / selection state when PostUndo is called, this makes sure we repopulate tooltips if need etc.
	#jira UE-38734

Change 3203748 on 2016/11/18 by Jurre.deBaare

	Crash Generating Proxy Meshes after replacing static meshes in the level
	#fix just calculate bounds for the used UVs (old behaviour was wrong)
	#jira UE-38764

Change 3203751 on 2016/11/18 by james.cobbett

	Changes to TM-PoseSnapshot and new test assets

Change 3203799 on 2016/11/18 by Thomas.Sarkanen

	Switched fudged auto-LOD calculations to use a pow() decay instead of a recprocal

	Still a fudge when LOD reduction has not been performed in-engine, but a fudge with similar outcomes to the previous method.
	Also fixed up the naming of some variables that still referred to screen areas & LOD distances.

	#jira UE-38674 - LOD distance switching have changed since 4.14 and merged lod actors seem to switch at incorrect screen scales as a result

Change 3203856 on 2016/11/18 by james.cobbett

	TM-PoseSnapshot - Rebuild lighting and updated anims

Change 3203880 on 2016/11/18 by Ori.Cohen

	Copying //UE4/Dev-Physics-Upgrade to Dev-Framework (//UE4/Dev-Framework)

Change 3203940 on 2016/11/18 by Ori.Cohen

	Fix missing newline for ps4

Change 3203960 on 2016/11/18 by Ori.Cohen

	Readd fix for linux macro expansion warning

Change 3203975 on 2016/11/18 by Ori.Cohen

	Fix for linux toolchain not knowing about no-unused-local-typedef

Change 3203989 on 2016/11/18 by Ori.Cohen

	Make sure physx automation doesn't try to build html5 APEX.

Change 3204031 on 2016/11/18 by james.cobbett

	Minor update to test level

Change 3204035 on 2016/11/18 by Marc.Audy

	Additional Attenuation refactor cleanup

Change 3204044 on 2016/11/18 by Ori.Cohen

	Fix typo of NV_SIMD_SSE2

Change 3204049 on 2016/11/18 by Ori.Cohen

	Fix missing newline for PS4 compiler

Change 3204463 on 2016/11/18 by mason.seay

	Finalized URO test map

Change 3204621 on 2016/11/18 by mason.seay

	Small improvements

Change 3204751 on 2016/11/18 by Ori.Cohen

	Make PhAT highlight selected bodies and constraints in the tree view

Change 3205868 on 2016/11/21 by Marc.Audy

	Merging //UE4/Dev-Main to Dev-Framework (//UE4/Dev-Framework) @ 3205744

Change 3205887 on 2016/11/21 by Jurre.deBaare

	Fix for similar crash in blendspace editor like UE-38734

Change 3206121 on 2016/11/21 by Marc.Audy

	PR #2935: Minor subtitle issues (Contributed by projectgheist)
	#jira UE-38803
	#jira UE-38692

Change 3206187 on 2016/11/21 by Marc.Audy

	PR #2935: Minor subtitle issues (Contributed by projectgheist)
	Additional bits
	#jira UE-38519
	#jira UE-38803
	#jira UE-38692

Change 3206318 on 2016/11/21 by Marc.Audy

	Fix Linux compiler whinging

Change 3206379 on 2016/11/21 by Marc.Audy

	Fix crash when streaming in a sublevel with a child actor in it (4.14.1)
	#jira UE-38906

Change 3206591 on 2016/11/21 by Marc.Audy

	Refactor restrictions to allow hidden and clarify disabled

Change 3206776 on 2016/11/21 by Marc.Audy

	ForceFeedback component allows rumble events to be placed or spawned in to the world with attenuation settings that dictate how intensely the rumble pattern will be applied to the player based on their distance to the effect.
	ForceFeedback Attenuation settings can be defined via the content browser or directly on the component.
	#jira UEFW-244

Change 3206901 on 2016/11/21 by Marc.Audy

	Fix compile error in automation tests

Change 3207235 on 2016/11/22 by danny.bouimad

	Updated Map

Change 3207264 on 2016/11/22 by Thomas.Sarkanen

	Disable bone editing in anim blueprint editor

	#jira UE-38876 - Transform options in bone Details panel in Anim Blueprint Persona editor appear editable

Change 3207303 on 2016/11/22 by Lina.Halper

	Clear material curve by setting it directly because the flag might not exist

	#jira: UE-36902

Change 3207331 on 2016/11/22 by Jon.Nabozny

	Fix overflow issues in SerializeProperties_DynamicArray_r. Also, fix crash from not ensuring properties were serialized successfully.

Change 3207357 on 2016/11/22 by Danny.Bouimad

	Updating testcontent for pose drivers

Change 3207425 on 2016/11/22 by Lina.Halper

	Fix frame count issue with montage

	#jira: UE-30048

Change 3207478 on 2016/11/22 by Lina.Halper

	Fix so that curve warning doesn't happen when your name is same.
	#jira: UE-34246

Change 3207526 on 2016/11/22 by Marc.Audy

	Fix crash when property restriction introduces a hidden entry

Change 3207731 on 2016/11/22 by danny.bouimad

	MoreUpdates

Change 3207764 on 2016/11/22 by Lina.Halper

	#fix order of morphtarget to first process animation and then BP for slave component

Change 3207842 on 2016/11/22 by Ben.Zeigler

	Fix it so ActiveStructRedirects are checked in addition to ActiveClassRedirects when serializing a raw UStruct reference, such as in a blueprint UStructProperty. This fixes issue with the attenuation settings struct rename, and should have always been working this way. ActiveClassRedirects will still work.

Change 3208202 on 2016/11/22 by Ben.Zeigler

	#jira UE-38811 Fix regression with gimbal locking in player camera manager.
	The quat->rotator->quat->rotator conversions are introducing more error than in 4.13, so a pitch limit of -89.99 was too precise.

Change 3208510 on 2016/11/23 by Wes.Hunt

	Disable UBT Telemetry on internal builds #jira AN-1059
	#tests build a few different ways, add more diagnostics to clarify if the provider is being used.

Change 3208734 on 2016/11/23 by Martin.Wilson

	Change EnsureAllIndicesHaveHandles to try and maintain validity of as many of the handles as possible + Make FRichCurve key member private as it needs to stay in sync with map on base class

	#jira UE-38899

Change 3208782 on 2016/11/23 by Thomas.Sarkanen

	Fixed material and vert count issues with skeletal to static mesh conversion

	Material remapping was not bein gbuilt, so material indices were overwitten inappropriately.
	Vertex tangentY was being recalculated incorrectly (discarding the W component when transformed), so vertices were not correctly re-merged later in the static mesh build phase.

	#jira UE-37898 - Materials are incorrect on static mesh made from skeletal mesh

Change 3208798 on 2016/11/23 by James.Golding

	UE-38478 - Fix collision on procmesh created in BeginPlay in cooked builds

Change 3208801 on 2016/11/23 by Jurre.deBaare

	Hidden Material References from Mesh Components Fix
	#fix forgot to mark the renderstate dirty and wrapped it to only apply when overridematerials actually contain something
	#jira UE-38108

Change 3208807 on 2016/11/23 by Thomas.Sarkanen

	CIS fix

Change 3208824 on 2016/11/23 by danny.bouimad

	More content updates for Testing

Change 3208827 on 2016/11/23 by Danny.Bouimad

	Removing Old Pose driver Testassets I created awhile ago.

Change 3209026 on 2016/11/23 by Martin.Wilson

	CIS Fix for FRichCurve

Change 3209083 on 2016/11/23 by Marc.Audy

	Don't crash if after an undo the previously selected object no longer exists (4.14.1)
	#jira UE-38991

Change 3209085 on 2016/11/23 by Marc.Audy

	Don't crash if a negative length passed in to UKismetStringLibrary::GetSubstring (4.14.1)
	#jira UE-38992

Change 3209124 on 2016/11/23 by Ben.Zeigler

	#jira UE-38867 Fix some game mode log messages
	From PR #2955

Change 3209231 on 2016/11/23 by Marc.Audy

	Auto removal

Change 3209232 on 2016/11/23 by Marc.Audy

	GetComponents now optionally can include components in Child Actors

Change 3209233 on 2016/11/23 by Marc.Audy

	ParseIntoArray resets instead of empty

Change 3209235 on 2016/11/23 by Marc.Audy

	Allow child actor components to be selected in viewports
	Fix selection highlight not working on nested child actors
	#jira UE-16688

Change 3209247 on 2016/11/23 by Marc.Audy

	Merging //UE4/Dev-Main to Dev-Framework (//UE4/Dev-Framework) @ 3209194

Change 3209299 on 2016/11/23 by Marc.Audy

	Use MoveTemp to reduce some memory churn in graph schema actions

Change 3209347 on 2016/11/23 by Marc.Audy

	Don't dispatch a tick function that had been scheduled but has been disabled before being executed.
	#jira UE-37459

Change 3209507 on 2016/11/23 by Ben.Zeigler

	#jira UE-38185 Keep player controllers in their same order during a seamless travel
	From PR #2908

Change 3209882 on 2016/11/24 by Thomas.Sarkanen

	Copy-to-array now works with the fast path

	Refactored the copy record generation/validation code to be clearer with better seperation of concerns.
	Made sure we always properly generate a full exec chain for our events, despite some other them potentially using the fast path (this may have been a bug waiting to happen).
	Fixed a potentiual bug with sub anim instances were potentiall fast path non-array properties were skipped.
	Added tests for fast path validity to EditorTests project. Assets to follow.

	#jira UE-34569 - Fast Path gets turned off if you link to multiple input pins

Change 3209884 on 2016/11/24 by Thomas.Sarkanen

	File I missed

Change 3209885 on 2016/11/24 by Thomas.Sarkanen

	Support assets for fast path tests

Change 3209939 on 2016/11/24 by Benn.Gallagher

	Fixed anim blueprint compiler not following reroute nodes when building cached pose fragment list
	#jira UE-35557

Change 3209941 on 2016/11/24 by Jurre.deBaare

	Removing and readding a point to the Anim Offset graph results in the animation to not preview correctly.
	#fix make sure that when we delete a sample point we reset the preview base pose
	#misc changed how the preview base pose is determined/updated
	#jira UE-38733

Change 3209942 on 2016/11/24 by Thomas.Sarkanen

	Fixed transactions being made when setting bone space in details panel

	Also added reset to defaults to allow easy removal of bone modifications.

	#jira UE-38957 - Switching between Local and World Location in Persona Bone Transform options creates an Undo transaction

Change 3209945 on 2016/11/24 by james.cobbett

	Test assets for Pose Snapshot Test Case

Change 3210239 on 2016/11/25 by Mieszko.Zielinski

	Making Navmesh react to changes done to static mesh's collision setup via the SM Editor #UE4

	#jira UE-29415

Change 3210279 on 2016/11/25 by Benn.Gallagher

	Fixed anim sub-instances only allowing one pin to work when any pin required a call out to the VM for evaluation
	#jira UE-38040

Change 3210288 on 2016/11/25 by danny.bouimad

	Cleaned up Pose Driver Anim BP's

Change 3210334 on 2016/11/25 by Benn.Gallagher

	Fixed preview mesh references getting broken in physics assets when renaming the preview mesh asset. Added explicit reference collection for the TAssetPtr
	#jira UE-22145

Change 3210349 on 2016/11/25 by James.Golding

	UE-35783 Fix scrolling in PoseAsset editor panels

Change 3210356 on 2016/11/25 by James.Golding

	UE-38420 Disable 'Convert to Static Mesh' option if no MeshComponents selected (e.g. cables)

Change 3210357 on 2016/11/25 by Jurre.deBaare

	Numeric textbox value label incorrect for aimoffset/blendspaces in grid
	#fix change lambda capture type (was referencing local variable)

Change 3210358 on 2016/11/25 by Jurre.deBaare

	Crash Generating Proxy Mesh with Transition Screen Size set to 1
	#fix 1.0 was not included within the possible range
	#jira UE-38810

Change 3210364 on 2016/11/25 by James.Golding

	Improve BuildVertexBuffers to use stride and avoid copying colors

Change 3210371 on 2016/11/25 by Jurre.deBaare

	You can no longer enable tooltip display when using anim offset
	#fix Added back ability to show advanced preview sample weighting to tooltip under CTRL down
	#jira UE-38808

	It's not clear that the user has to hold shift to preview in blend spaces
	#fix Preview value is now set by default and has a tooltip state, this will inform the user how to move the preview value
	#jira UE-38711

	#misc refactored out some duplicate code :)

Change 3210387 on 2016/11/25 by james.cobbett

	Updating test asset

Change 3210550 on 2016/11/26 by Marc.Audy

	Merging //UE4/Dev-Main to Dev-Framework (//UE4/Dev-Framework) @ 3209927

	Brings IWYU in and required substantial fixups

Change 3210551 on 2016/11/26 by Marc.Audy

	Delete empty cpp files

Change 3211002 on 2016/11/28 by Lukasz.Furman

	added navigation update on editting volume's brush
	#ue4

Change 3211011 on 2016/11/28 by Marc.Audy

	Roll back CL# 3210334 as it is causing deadlocks during GC

Change 3211039 on 2016/11/28 by Jurre.deBaare

	Merge Actors tool is splitting every vertex on spline meshes, causing hard edged vertex colors.
	#fix prevent using the wedge map when propagating spline mesh vertex colours
	#jira UE-36011

Change 3211053 on 2016/11/28 by Ori.Cohen

	Make sure objects without simple collision do not simulate. Fixes crash when two trimesh only objects collide

	#JIRA UE-38989

Change 3211101 on 2016/11/28 by mason.seay

	Adjusting trigger collision so it can't be triggered by projectiles

Change 3211171 on 2016/11/28 by Jurre.deBaare

	Previewing outside of Blendspace Graph points causes unexpected weighting
	#jira UE-32775
	Second Animation Sample added to AimOffset or Blendspace swaps with the first sample
	#jira UE-36755

	#fix Changed behaviour for calculating blendspace grid weighting for one, two or colinear triangles
	- One: fill grid weights to single sample
	- Two: find closest point on line between the two samples for the grid point, and weight according to the distance on the line
	- Colinear: find two closest samples and apply behaviour above
	#misc rename variables to make the code more clear and correct

Change 3211491 on 2016/11/28 by Marc.Audy

	Provide proper tooltip for GetParentActor/Component
	Expose GetAttachParentActor/SocketName to blueprints
	De-virtualize Actor GetAttach... functions
	#jira UE-39056

Change 3211570 on 2016/11/28 by Lina.Halper

	Title doesn't update when asset is being dropped

	#jira: UE-39019

Change 3211766 on 2016/11/28 by Ori.Cohen

	Remove warning when a constraint has two empty components. This can be a valid usecase for when components are determined dynamically.

	#JIRA UE-36089

Change 3211938 on 2016/11/28 by Mason.Seay

	CSV's for testing gameplay tags

Change 3212090 on 2016/11/28 by Ori.Cohen

	Expose angular SLERP drive to blueprints

	#JIRA UE-36690

Change 3212102 on 2016/11/28 by Marc.Audy

	Fix shadow variable issue
	#jira UE-39099

Change 3212182 on 2016/11/28 by Ori.Cohen

	PR #2902: Fix last collision preset display (Contributed by max99x)

	#JIRA UE-38100

Change 3212196 on 2016/11/28 by dan.reynolds

	AEOverview Update:

	Minor tweaks and fixes

	Added Attenuation Curve Tests

	Renamed SC to SCLA for Sound Class prefix

	WIP SCON (Sound Concurrency)

Change 3212347 on 2016/11/28 by Ben.Zeigler

	#jira UE-39098 Fix issues with adding tag redirectors with the editor open, it now checks the redirector list in the editor
	Fix chained tag redirectors to work properly
	Const fixes and removed a bad error message spam, and fix rename message

Change 3212385 on 2016/11/28 by Marc.Audy

	Avoid duplicate GetWorld() calls

Change 3212386 on 2016/11/28 by Marc.Audy

	auto shoo

Change 3213018 on 2016/11/29 by Marc.Audy

	Fix shadow variable for real

Change 3213037 on 2016/11/29 by Ori.Cohen

	Fix deprecation warnings

Change 3213039 on 2016/11/29 by Marc.Audy

	Generalize logic for when a component prevents an Actor from auto destroying
	Add forcefeedback component to the components that will hold up the auto destroy of an actor

Change 3213088 on 2016/11/29 by Marc.Audy

	Move significance manager out of experimental

Change 3213187 on 2016/11/29 by Marc.Audy

	Add InsertDefaulted to mirror options available when Adding

Change 3213254 on 2016/11/29 by Marc.Audy

	add auto-complete for showdebug forcefeedback

Change 3213260 on 2016/11/29 by Marc.Audy

	Allow systems to inject auto-complete console entries

Change 3213276 on 2016/11/29 by Marc.Audy

	add auto-complete entry for showdebug significancemanager

Change 3213331 on 2016/11/29 by James.Golding

	Split SkeletalMesh skin weights into their own stream
	Remove unused FGPUSkinVertexColor struct
	Remove unused FSkeletalMeshVertexBuffer::bInfluencesByteSwapped bool
	Fix FSkeletalMeshMerge::GenerateLODModel to handle >4 weights
	Update friendly name for FColorVertexBuffer now it's used by skel mesh as well

Change 3213349 on 2016/11/29 by Ben.Zeigler

	Fix tag rename feedback message

Change 3213355 on 2016/11/29 by Ben.Zeigler

	#jira UE-39115 PR #2987: Added IsPaused to AGameModeBase (Contributed by RoyAwesome)

Change 3213406 on 2016/11/29 by Ori.Cohen

	Make sure body transforms are not set while the physx simulation is running.

	#JIRA UE-37270

Change 3213508 on 2016/11/29 by Jurre.deBaare

	When performing a merge actor on an actor merging multiple materials certain maps aren't generated
	#fix Apparently rendering out specular etc now outputs its value only to the red channel, so had to change how we populate the combined metallic/roughness/specular map
	#jira UE-38526

Change 3213557 on 2016/11/29 by Ben.Zeigler

	#jira UE-22145 Fix issues where TAssetPtrs weren't getting properly fixed up during rename fixup, it now runs the StringAssetReference fixup on the nested reference. This should fix lots of weird issues with references going away

Change 3213634 on 2016/11/29 by Ori.Cohen

	Make sure if no shapes are found for vehicle wheels we create spheres and attach them to the actor.

Change 3213639 on 2016/11/29 by Ori.Cohen

	Fix from nvidia for vehicle suspension exploding when given a bad normal.

	#JIRA UE-38716

Change 3213812 on 2016/11/29 by James.Golding

	UE-35925 Remove hard-coded asset<->animnode mapping, add SupportsAssetClass virtual instead

Change 3213824 on 2016/11/29 by Ori.Cohen

	Fix CIS

Change 3213873 on 2016/11/29 by Ori.Cohen

	Fix welded bodies not properly computing mass properties.

	#JIRA UE-35184

Change 3213950 on 2016/11/29 by Mieszko.Zielinski

	Fixed navigation collision being generated wrong for StaticMeshes created from BSP #Orion

	#jira UE-37221

Change 3213951 on 2016/11/29 by Mieszko.Zielinski

	Fixed perception system having issue with registering perception listener spawned in sublevels #UE4

	#jira UE-37850

Change 3214005 on 2016/11/29 by Ori.Cohen

	Fix mass kg override not propagating to blueprint instances.

Change 3214046 on 2016/11/29 by Marc.Audy

	Duplicate all instanced subobjects, not just those that are editinlinenew
	Make AABrush.Brush instanced rather than export
	#jira UE-39066

Change 3214064 on 2016/11/29 by Marc.Audy

	Use GetComponents directly where safe instead of copying in to an array

Change 3214116 on 2016/11/29 by James.Golding

	Fix tooltip when dragging anim assets onto players

Change 3214136 on 2016/11/29 by Ori.Cohen

	Make it so moving bodies is immediate when in editor. Useful for editor tools that rely on physx data

	#JIRA UE-35864

Change 3214162 on 2016/11/29 by Mieszko.Zielinski

	Fixed a bug in EnvQueryGenerator_SimpleGrid resuting in one extra column and row of points being generated #UE4

	#jira UE-12077

Change 3214177 on 2016/11/29 by Marc.Audy

	Use correct SocketName (broken in CL#2695130)
	#jira UE-39153

Change 3214427 on 2016/11/29 by dan.reynolds

	AEOverview Update

	Fixed Attenuation tests when overlapping attenuation ranges between streamed levels

	Added Sound Concurrency Far then Prevent New testmap

	Removed some Sound Concurrency assets

Change 3214469 on 2016/11/29 by dan.reynolds

	AEOverview Update

	Added Sound Concurrency Test for Stop Farthest then Oldest

Change 3214842 on 2016/11/30 by Jurre.deBaare

	LookAt AimOffset in the Anim Graph causes character to explode
	#jira UE-38533
	#fix ensure that the source socket exists on the skeleton during compilation (as far as we can), and skip blendspace evaluation in case of it not being valid during runtime

Change 3214866 on 2016/11/30 by james.cobbett

	Updating Pose Snapshot test assets

Change 3214964 on 2016/11/30 by thomas.sarkanen

	Added test data for facial animtion curves

Change 3215015 on 2016/11/30 by Jurre.deBaare

	When a Aim Offset axis value is edited drastically the preview mesh will be deformed
	#fix change the way we change data when axis values are changed, simply remap normalized samples to new axis range
	#misc marked some data/functions editor only (not needed during runtime so reduces footprint a little bit)
	#jira UE-38880

Change 3215029 on 2016/11/30 by Marc.Audy

	Fix CIS

Change 3215033 on 2016/11/30 by Marc.Audy

	Add a delegate for when new classes are added via hotreload
	Change existing hotload class reinstancing delegates to be multicast

Change 3215048 on 2016/11/30 by Jon.Nabozny

	Use getKinematicTarget whenever a body is kinematic.
	This should fix some edge cases in FBodyInstance where stale transforms may be used when operations are run in PrePhysics.

	#jira UE-37877

Change 3215052 on 2016/11/30 by Marc.Audy

	Generalize the volume actor factory logic
	Create volume factories when hotreload adds a new volume class
	#jira UE-39064

Change 3215055 on 2016/11/30 by Marc.Audy

	Probable fix for IOS CIS failure

Change 3215091 on 2016/11/30 by Lina.Halper

	Easy alternative fix for blending two curves per bone. For now we just combine.

	To fix this properly - i.e. per bone to affect curve - it is very expensive process, so opting into this for 4.15.

	#jira: UE-39182

Change 3215179 on 2016/11/30 by Jurre.deBaare

	Preview viewport should only use rendering features supported in project
	#fix replace the skylight with a sphere reflection component, this will not give image based lighting but does supply the user with a reflection map + intensity
	#jira UE-37252

Change 3215189 on 2016/11/30 by Jurre.deBaare

	CIS fix

Change 3215326 on 2016/11/30 by Ben.Zeigler

	#jira UE-39077 Fix OnActive gameplay cues on standalone servers, it was incorrectly assuming it was in mixed replication mode.
	Regression caused by CL #3104976

Change 3215523 on 2016/11/30 by James.Golding

	Fix cooking old skel meshes in commandlet - vertex buffer was not recreated so UpdateUVChannelData would crash

Change 3215539 on 2016/11/30 by Marc.Audy

	Fix failure to cleanup objects in a hidden always loaded sub-level
	#jira UE-39139

Change 3215568 on 2016/11/30 by Aaron.McLeran

	UE-39197 Delay node of 0.0 causes crash

Change 3215719 on 2016/11/30 by Aaron.McLeran

	UE-39074 Audio related Client crash experienced on latest live build ++UT+Release-Next-CL-3193528

Change 3215773 on 2016/11/30 by Aaron.McLeran

	PR #2819 : Fixed typo in SoundWave.h

Change 3215828 on 2016/11/30 by James.Golding

	PR #2900: fixed a former change that overlooked the 2 character difference between 16 and 32. (Contributed by MartinMittringAtOculus)

Change 3215831 on 2016/11/30 by James.Golding

	UE-36688 Add BlendOption (with CustomCurve) to PoseBlendNode

Change 3215904 on 2016/11/30 by Marc.Audy

	Fix significance calculations

Change 3215955 on 2016/11/30 by James.Golding

	UE-36791 Fix scaling of rotated convex elements, by baking element transform into cooked convex data.

Change 3215959 on 2016/11/30 by James.Golding

	Remove LogTemp warning from FAnimBlueprintCompiler::FinishCompilingClass

Change 3216057 on 2016/11/30 by Marc.Audy

	Don't reset expose on spawn properties when in a PIE world
	#jira UE-36771

Change 3216114 on 2016/11/30 by James.Golding

	Move SkeletalMeshComponent and SkinnedMeshComponent functions out of SkeletalMesh.cpp into correct cpp files

Change 3216144 on 2016/11/30 by Jon.Nabozny

	Fix FConstraintInstance scaling issues in FSkeletalMeshComponent::InitArticulated.

	InitArticulated uses the default Constraint Template from the Physics Asset a skeletal mesh is associated with.
	This caused issues if a skeletal mesh had bone scales that differed from those in the physics asset.

	#jira UE-38434

Change 3216148 on 2016/11/30 by Jon.Nabozny

	Create test map and asset for Skeletal Mesh Component Scaling and Skeletal Mesh Uniform Import Scaling.

Change 3216160 on 2016/11/30 by Aaron.McLeran

	Fixing a memory leak in concurrency management

Change 3216164 on 2016/11/30 by James.Golding

	Move SkeletalMeshActor code into its own cpp file
	Fix CIS for SkeletalMeshComponent.cpp

Change 3216371 on 2016/11/30 by dan.reynolds

	AEOverview Update

	Minor tweaks

	Completed Sound Concurrency Rule Test Maps

	Added additional test files

Change 3216509 on 2016/11/30 by Marc.Audy

	Fix missing include

Change 3216510 on 2016/11/30 by Marc.Audy

	Code cleanup

Change 3216723 on 2016/12/01 by Jurre.deBaare

	When clearing a blend sample animation the animation will try and blend to the ref pose
	#fix do not delete sample when animation == nullptr but mark it as invalid, it then will be rendered in red on the grid and discarded during triangle/line generation
	#fix indice mapping for 2d blend spaces was incorrect before (luckily never caused an error)
	#misc weird whitespace changes
	#jira UE-39078

Change 3216745 on 2016/12/01 by Jurre.deBaare

	- Blend space triangulation was incorrect in some cases, due to refactor some data was not initialised.
	- UDN user was hitting a check within the triangle flipping behaviour

	#fix Revisited the conditions to determine whether or not a point lies within a triangles circumcircle
	#fix In case we cannot flip the current triangle we skip it and move onto the next one instead of putting in a hard check
	#misc refactored triangle flipping code to make it smaller (more readible)

Change 3216903 on 2016/12/01 by mason.seay

	Imported mesh for quick test

Change 3216904 on 2016/12/01 by Jurre.deBaare

	CIS Fix
	#fix replaced condition by both non-editor as editor valid one

Change 3216998 on 2016/12/01 by Lukasz.Furman

	fixed AI slowing down on ramps due to 3D input vector being constrained by movement component
	#jira UE-39233
	#2998

Change 3217012 on 2016/12/01 by Lina.Halper

	Checking in James' fix on drag/drop to replace assets

	#code review: James.Golding
	#jira: UE-39150

Change 3217031 on 2016/12/01 by james.cobbett

	Updating Pose Snapshot Assets. Again.

Change 3217033 on 2016/12/01 by Martin.Wilson

	Update bounds on all skel meshes when physics asset is changed

	#jira UE-38572

Change 3217181 on 2016/12/01 by Martin.Wilson

	Fix imported animations containing a black thumbnail

	#jira UE-36559

Change 3217183 on 2016/12/01 by Martin.Wilson

	Add some extra debugging code for future animation compression / ddc issues

Change 3217184 on 2016/12/01 by james.cobbett

	Fixing a test asset by checking a check box. Sigh.

Change 3217216 on 2016/12/01 by Martin.Wilson

	Undo part of CL 3217183. Will need to add this back differently.

Change 3217274 on 2016/12/01 by Marc.Audy

	When serializing in an enum tagged property follow redirects
	#jira UE-39215

Change 3217419 on 2016/12/01 by james.cobbett

	Changes to test assets for more Pose Snapshot tests

Change 3217449 on 2016/12/01 by Aaron.McLeran

	Adding new audio setting to disable EQ and reverb.

	Hooked up to XAudio2 (for now).

Change 3217513 on 2016/12/01 by Marc.Audy

	Improve bWantsBeginPlay deprecation message

Change 3217620 on 2016/12/01 by mason.seay

	Updated test assets for HLOD

Change 3217872 on 2016/12/01 by Aaron.McLeran

	UEFW-113 Adding master reverb to audio mixer

	- Added new submix editor to create new submixes
	- Created new default master submixes for reverb and EQ and master submixes
	- Fixed a number of minor issues found in auido mixer while working on feature

Change 3218053 on 2016/12/01 by Ori.Cohen

	Added mass debug rendering

	#JIRA UE-36608

Change 3218143 on 2016/12/01 by Aaron.McLeran

	Fixing up reverb to support multi-channel (5.1 and 7.1) configurations.

	- Added default reverb send amount

Change 3218440 on 2016/12/01 by Zak.Middleton

	#ue4 - Made some static FNames const.

Change 3218715 on 2016/12/02 by james.cobbett

	Fixed bug in test asset.

Change 3218836 on 2016/12/02 by james.cobbett

	Fixing up test asset

Change 3218884 on 2016/12/02 by james.cobbett

	Moar test asset changes

Change 3218943 on 2016/12/02 by Ori.Cohen

	Make sure welded bodies include the center of mass offset. Note this also changes the COM nudge to be world space instead of local space

	#JIRA UE-35184

Change 3218955 on 2016/12/02 by Marc.Audy

	Fix initialization order issues
	Remove monolithic includes
	Change signature to pass string by const ref

Change 3219149 on 2016/12/02 by Ori.Cohen

	Fix SetCollisionObjectType not working on skeletal mesh components

	#JIRA UE-37821

Change 3219162 on 2016/12/02 by Martin.Wilson

	Fix compile error when blend space on aim offset nodes is exposed as pin

	#jira UE-39285

Change 3219198 on 2016/12/02 by Marc.Audy

	UEnum::FindValue/IndexByName will now correctly follow redirects
	#jira UE-39215

Change 3219340 on 2016/12/02 by Zak.Middleton

	#ue4 - Optimized and cleaned up some Actor methods related to location and rotation.

	- Inlined GetActorForwardVector(), GetActorUpVector(), GetActorRightVector(). Wrapped them to simply call the methods on USceneComponent rather than using a different approach to computing these vectors.
	- Inlined blueprint versions: K2_GetActorLocation(), K2_GetActorRotation(), K2_GetRootComponent().
	- Cleaned up template methods that are used to delay compilation of USceneComponent calls to make them private and prefix "Template" to their names so they don't show up in autocomplete for calls to the public methods.

Change 3219482 on 2016/12/02 by Ori.Cohen

	Fix crash when double deleting a clothing actor due to destroying USkeletalMesh before USkeletalMeshComponent.

	#JIRA UE-39172

Change 3219676 on 2016/12/02 by Martin.Wilson

	Make clearer that ref pose is from skeleton

Change 3219687 on 2016/12/02 by Aaron.McLeran

	Supporting multi-channel reverb with automatic downmixing of input to stereo

Change 3219688 on 2016/12/02 by Martin.Wilson

	Fix crash when remapping additive animations after skeleton hierarchy change

	#jira UE-39040

Change 3219699 on 2016/12/02 by Zak.Middleton

	#ue4 - Fix template's use of old GetActorRotation() function.

Change 3219969 on 2016/12/02 by Ben.Zeigler

	#jira UE-24800 Disable replicatied movement updates for actors that are welded to something else, to avoid them fighting with the welded parent's replication
	Modified from shelve Zak.Middleton made of PR #1885, after some more testing

Change 3220010 on 2016/12/02 by Aaron.McLeran

	Fixing up sound class editor

Change 3220013 on 2016/12/02 by Aaron.McLeran

	Deleting monolithic file

Change 3220249 on 2016/12/02 by Aaron.McLeran

	Changing reverb settings parameter thread sync method

	- Switching to a simple ring buffer rather than using a crit sect

Change 3220251 on 2016/12/02 by Aaron.McLeran

	Removing hard-coded audio mixer module name for the case when using -audiomixer argument,

	-added new entry to ini file that allows you to specify the audio mixer module name used for the platform.

Change 3221118 on 2016/12/05 by Jurre.deBaare

	Back out changelist 3220249 to fix CIS

Change 3221363 on 2016/12/05 by Martin.Wilson

	Change slot node category from Blends to Montage

Change 3221375 on 2016/12/05 by Jon.Nabozny

	Change AGameModeBase::GetGameSessionClass to return GameSessionClass when set.

	#jira UE-39325

Change 3221402 on 2016/12/05 by Jon.Nabozny

	Add sanitization code around PhsyX flags and refactor the ways flags are managed through a single code path.

	#jira UE-33562

Change 3221441 on 2016/12/05 by Thomas.Sarkanen

	Fixed crash when reimporting a mesh when a different animation was open

	#jira UE-39281 - Editor crashes when reimporting a skeletal mesh after enabling recalculate tangents

Change 3221473 on 2016/12/05 by Marc.Audy

	Get rid of auto.
	Use GetComponents directly instead of copying in to temporary arrays

Change 3221584 on 2016/12/05 by Jon.Nabozny

	Fix CIS for Mac builds from CL-3221375

Change 3221631 on 2016/12/05 by Martin.Wilson

	Possible fix for rare marker sync crash on live servers

	#jira UE-39235
	#test ai match, animation seemed fine, no crashes

Change 3221660 on 2016/12/05 by mason.seay

	Resubmitting to add Viewport Bookmark

Change 3221683 on 2016/12/05 by Mieszko.Zielinski

	Temp (but decent) fix to ARecastNavMesh::GetRandomPointInNavigableRadius sometimes retrieving invalid locations even if there's a valid piece of navmesh in the area #UE4

	#jira UE-30355

Change 3221750 on 2016/12/05 by Jon.Nabozny

	Real CIS fix.

Change 3221917 on 2016/12/05 by Jon.Nabozny

	Fix CIS for real this time.

Change 3222370 on 2016/12/05 by mason.seay

	Start of Gameplay Tag testmap

Change 3222396 on 2016/12/05 by Aaron.McLeran

	UEFW-44 Implementing EQ master submix effect for audio mixer

	- New thread safe param setting temlate class (for setting EQ and Reverb params)
	- Hook up reverb submix effect to source voices
	- Implementation of FBiquad for biquad filter coefficients and audioprocessing
	- Implementation of Filter class which hold FBiquad instance per channel, computes coefficents once
	- Implementation of equalizer class which is a serial bank of filters set to ParametricEQ filter type

Change 3222425 on 2016/12/05 by Aaron.McLeran

	Checking in missing files

Change 3222429 on 2016/12/05 by Aaron.McLeran

	Last missing file!

Change 3222783 on 2016/12/05 by Jon.Nabozny

	Update SkelMeshScaling map.

Change 3223173 on 2016/12/06 by Martin.Wilson

	Fix crash in thumbnail rendering when creating a new montage

	#jira UE-39352

Change 3223179 on 2016/12/06 by Marc.Audy

	auto/NULL cleanup

Change 3223329 on 2016/12/06 by Marc.Audy

	Fix (hard to explain) memory corruption
	#jira UE-39366

Change 3223334 on 2016/12/06 by Jon.Nabozny

	Add HasBeenInitialized check inside AActor::InitializeComponents

Change 3223340 on 2016/12/06 by Jon.Nabozny

	Refactor SkeletalMesh constraint scaling fixes. Add a check on bodies to ensure they are valid.

	#jira UE-39238

Change 3223372 on 2016/12/06 by Marc.Audy

	Probably fix HTML5 CIS failure

Change 3223511 on 2016/12/06 by Jon.Nabozny

	Fix Mac CIS shadow warning

Change 3223541 on 2016/12/06 by Lukasz.Furman

	fixed missing NavCollision data in static meshes
	#jira UE-39367

Change 3223672 on 2016/12/06 by Ben.Zeigler

	#jira UE-39394 Fix GameplayTagContainerCustomization to work like GameplayTagCustomization as a popup instead of a window, this fixes the references button
	Remove unnecessary code from both customizations

Change 3223751 on 2016/12/06 by Marc.Audy

	Properly remove components from their owner when manipulating through editinlinenew properties
	#jira UE-30548

Change 3223831 on 2016/12/06 by Ben.Zeigler

	#jira UE-39293 Don't show non-working tag operations when ini tag editing is not enabled
	#jira UE-39344 Improve feedback messages when deleting explicit tags that have other explicit tag children
	Don't allow deleting a leaf explicit tag whose implicit parent tags are still referenced and it is the only thing keeping them alive
	Add Tag Source to tooltip in management mode
	Fix RequestGameplayTagChildrenInDictionary to work properly

Change 3223862 on 2016/12/06 by Marc.Audy

	Hide deprecated attach functions for all games not just Paragon

Change 3224003 on 2016/12/06 by Marc.Audy

	Put behavior of player camera back to how it was prior to Ansel plugin support changes. Make photography only work a different way.
	#jira UE-39207

Change 3224602 on 2016/12/07 by Jurre.deBaare

	Crash on creating LODs with Medic
	#fix Added clamp for UVs -1024 to 1024
	#jira UE-37726

Change 3224604 on 2016/12/07 by Jurre.deBaare

	Fix for incorrect normal calculation in certain circumstances
	#fix Make sure we propagate the matrices to samples after we (re)calculated normals
	#fix Conditionally swap/inverse the vertex data buffers instead of always
	#fix Set preview mesh for alembic import animation sequences
	#misc removed commented out code and added debug code

Change 3224609 on 2016/12/07 by Jurre.deBaare

	Alembic Import Issues (skeletal) w. UVs and smoothing groups
	#fix Changed the way we populate smoothing group indices for alembic caches
	#misc removed commented out code, set base preview pose for alembic imported skeletal meshes / anim sequences
	#jira UE-36412

Change 3224783 on 2016/12/07 by James.Golding

	Support per-instance skeletal mesh vertex color override

Change 3224784 on 2016/12/07 by James.Golding

	Add skelmesh vert color override map. Fix my vert color material to work on skel mesh.

Change 3225131 on 2016/12/07 by Jurre.deBaare

	Crash when baking matrix animation when importing an alembic file as skeletal
	#fix condition whether or not to apply matrices had not been moved over in previous change
	#jira UE-39439

Change 3225491 on 2016/12/07 by Lina.Halper

	- Morphtarget fix on the first frame

	#jira: UE-37702

Change 3225597 on 2016/12/07 by mason.seay

	Updated materials on meshes to ones that don't have physical materials, also rebuilt lighting

Change 3225758 on 2016/12/07 by Aaron.McLeran

	UE-39421 Fix for sound class graph bug

Change 3225957 on 2016/12/07 by Ben.Zeigler

	#jira UE-39433 Fix crash with mass debug data

Change 3225967 on 2016/12/07 by Lina.Halper

	Fix not removing link up cache when removed.

	#jira: UE-33738

Change 3225990 on 2016/12/07 by Ben.Zeigler

	#jira OR-32975 Sort gameplay tags before saving out modified ini, to help with merge issues

Change 3226123 on 2016/12/07 by Aaron.McLeran

	Fix for sound class asset creation from within the sound class graph

Change 3226165 on 2016/12/07 by mason.seay

	Replaced skelmesh gun with static mesh cube

Change 3226336 on 2016/12/07 by Aaron.McLeran

	Fixing up sound class replacement code.

	If you delete a sound class but replace with another, now it properly replaces sound classes in the sound class graphs without totally destroying them

Change 3226701 on 2016/12/08 by Thomas.Sarkanen

	Merging //UE4/Dev-Main to Dev-Framework (//UE4/Dev-Framework) @ CL 3226613

Change 3226710 on 2016/12/08 by Jurre.deBaare

	Fix for alembic import crash
	#misc update num mesh samples and take into account user set start frame in case of skipping preroll frames

Change 3226834 on 2016/12/08 by Jurre.deBaare

	Fix for incorrect matrix samples being applied during Alembic cache importing
	#fix Change way we loop through samples and determine correct matrix and mesh sample indices

Change 3227330 on 2016/12/08 by Jurre.deBaare

	Temporary fix for animBP compilation error, underlying issue is causing the skeleton to not be fully loaded when we are validating the animation node. This makes the socket name check fail and consequently output a compilation error
	#UE-39499

	#fix Ensure that the skeleton is loaded by checking for RF_NeedPostLoad
	#misc corrected socket name output, removed unnecessary nullptr check

Change 3227575 on 2016/12/08 by Marc.Audy

	Merging //UE4/Dev-Main to Dev-Framework (//UE4/Dev-Framework) @ 3227387

Change 3227602 on 2016/12/08 by Marc.Audy

	Copyright 2016 to 2017 updates for new Framework files

[CL 3227721 by Marc Audy in Main branch]
2016-12-08 16:58:18 -05:00

3695 lines
129 KiB
C++

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "GameProjectUtils.h"
#include "Misc/Guid.h"
#include "UObject/Class.h"
#include "FeaturePackContentSource.h"
#include "TemplateProjectDefs.h"
#include "HAL/PlatformFilemanager.h"
#include "Misc/MessageDialog.h"
#include "HAL/FileManager.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/ScopedSlowTask.h"
#include "Misc/App.h"
#include "Misc/EngineVersion.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SWindow.h"
#include "Framework/Application/SlateApplication.h"
#include "Components/ActorComponent.h"
#include "Components/SceneComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Pawn.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "ISourceControlOperation.h"
#include "SourceControlOperations.h"
#include "ISourceControlProvider.h"
#include "ISourceControlModule.h"
#include "GeneralProjectSettings.h"
#include "GameFramework/Character.h"
#include "Misc/FeedbackContext.h"
#include "UObject/UObjectHash.h"
#include "UObject/UObjectIterator.h"
#include "GameFramework/GameModeBase.h"
#include "UnrealEdMisc.h"
#include "PluginDescriptor.h"
#include "Interfaces/IPluginManager.h"
#include "ProjectDescriptor.h"
#include "Interfaces/IProjectManager.h"
#include "GameProjectGenerationLog.h"
#include "DefaultTemplateProjectDefs.h"
#include "SNewClassDialog.h"
#include "FeaturedClasses.inl"
#include "Interfaces/IMainFrameModule.h"
#include "AnalyticsEventAttribute.h"
#include "Interfaces/IAnalyticsProvider.h"
#include "EngineAnalytics.h"
#include "DesktopPlatformModule.h"
#include "Interfaces/ITargetPlatform.h"
#include "Interfaces/ITargetPlatformManagerModule.h"
#include "Styling/SlateIconFinder.h"
#include "SourceCodeNavigation.h"
#include "Misc/UProjectInfo.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Misc/HotReloadInterface.h"
#include "Dialogs/SOutputLogDialog.h"
#include "Sound/SoundEffectSubmix.h"
#include "Sound/SoundEffectSource.h"
#include "PlatformInfo.h"
#include "Blueprint/BlueprintSupport.h"
#define LOCTEXT_NAMESPACE "GameProjectUtils"
#define MAX_PROJECT_PATH_BUFFER_SPACE 130 // Leave a reasonable buffer of additional characters to account for files created in the content directory during or after project generation
#define MAX_PROJECT_NAME_LENGTH 20 // Enforce a reasonable project name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH
static_assert(PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE > 0, "File system path shorter than project creation buffer space.");
#define MAX_CLASS_NAME_LENGTH 32 // Enforce a reasonable class name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH
TWeakPtr<SNotificationItem> GameProjectUtils::UpdateGameProjectNotification = NULL;
TWeakPtr<SNotificationItem> GameProjectUtils::WarningProjectNameNotification = NULL;
FString GameProjectUtils::DefaultFeaturePackExtension(TEXT(".upack"));
FText FNewClassInfo::GetClassName() const
{
switch(ClassType)
{
case EClassType::UObject:
return BaseClass ? BaseClass->GetDisplayNameText() : FText::GetEmpty();
case EClassType::EmptyCpp:
return LOCTEXT("NoParentClass", "None");
case EClassType::SlateWidget:
return LOCTEXT("SlateWidgetParentClass", "Slate Widget");
case EClassType::SlateWidgetStyle:
return LOCTEXT("SlateWidgetStyleParentClass", "Slate Widget Style");
case EClassType::UInterface:
return LOCTEXT("UInterfaceParentClass", "Unreal Interface");
default:
break;
}
return FText::GetEmpty();
}
FText FNewClassInfo::GetClassDescription(const bool bFullDescription/* = true*/) const
{
switch(ClassType)
{
case EClassType::UObject:
{
if(BaseClass)
{
FString ClassDescription = BaseClass->GetToolTipText(/*bShortTooltip=*/!bFullDescription).ToString();
if(!bFullDescription)
{
int32 FullStopIndex = 0;
if(ClassDescription.FindChar('.', FullStopIndex))
{
// Only show the first sentence so as not to clutter up the UI with a detailed description of implementation details
ClassDescription = ClassDescription.Left(FullStopIndex + 1);
}
// Strip out any new-lines in the description
ClassDescription.ReplaceInline(TEXT("\n"), TEXT(" "));
}
return FText::FromString(ClassDescription);
}
}
break;
case EClassType::EmptyCpp:
return LOCTEXT("EmptyClassDescription", "An empty C++ class with a default constructor and destructor.");
case EClassType::SlateWidget:
return LOCTEXT("SlateWidgetClassDescription", "A custom Slate widget, deriving from SCompoundWidget.");
case EClassType::SlateWidgetStyle:
return LOCTEXT("SlateWidgetStyleClassDescription", "A custom Slate widget style, deriving from FSlateWidgetStyle, along with its associated UObject wrapper class.");
case EClassType::UInterface:
return LOCTEXT("UInterfaceClassDescription", "A UObject Interface class, to be implemented by other UObject-based classes.");
default:
break;
}
return FText::GetEmpty();
}
const FSlateBrush* FNewClassInfo::GetClassIcon() const
{
// Safe to do even if BaseClass is null, since FindIconForClass will return the default icon
return FSlateIconFinder::FindIconBrushForClass(BaseClass);
}
FString FNewClassInfo::GetClassPrefixCPP() const
{
switch(ClassType)
{
case EClassType::UObject:
return BaseClass ? BaseClass->GetPrefixCPP() : TEXT("");
case EClassType::EmptyCpp:
return TEXT("");
case EClassType::SlateWidget:
return TEXT("S");
case EClassType::SlateWidgetStyle:
return TEXT("F");
case EClassType::UInterface:
return TEXT("U");
default:
break;
}
return TEXT("");
}
FString FNewClassInfo::GetClassNameCPP() const
{
switch(ClassType)
{
case EClassType::UObject:
return BaseClass ? BaseClass->GetName() : TEXT("");
case EClassType::EmptyCpp:
return TEXT("");
case EClassType::SlateWidget:
return TEXT("CompoundWidget");
case EClassType::SlateWidgetStyle:
return TEXT("SlateWidgetStyle");
case EClassType::UInterface:
return TEXT("Interface");
default:
break;
}
return TEXT("");
}
FString FNewClassInfo::GetCleanClassName(const FString& ClassName) const
{
FString CleanClassName = ClassName;
switch(ClassType)
{
case EClassType::SlateWidgetStyle:
{
// Slate widget style classes always take the form FMyThingWidget, and UMyThingWidgetStyle
// if our class ends with either Widget or WidgetStyle, we need to strip those out to avoid silly looking duplicates
if(CleanClassName.EndsWith(TEXT("Style")))
{
CleanClassName = CleanClassName.LeftChop(5); // 5 for "Style"
}
if(CleanClassName.EndsWith(TEXT("Widget")))
{
CleanClassName = CleanClassName.LeftChop(6); // 6 for "Widget"
}
}
break;
default:
break;
}
return CleanClassName;
}
FString FNewClassInfo::GetFinalClassName(const FString& ClassName) const
{
const FString CleanClassName = GetCleanClassName(ClassName);
switch(ClassType)
{
case EClassType::SlateWidgetStyle:
return FString::Printf(TEXT("%sWidgetStyle"), *CleanClassName);
default:
break;
}
return CleanClassName;
}
bool FNewClassInfo::GetIncludePath(FString& OutIncludePath) const
{
switch(ClassType)
{
case EClassType::UObject:
if(BaseClass && BaseClass->HasMetaData(TEXT("IncludePath")))
{
OutIncludePath = BaseClass->GetMetaData(TEXT("IncludePath"));
return true;
}
break;
case EClassType::SlateWidget:
OutIncludePath = "Widgets/SCompoundWidget.h";
return true;
case EClassType::SlateWidgetStyle:
OutIncludePath = "Styling/SlateWidgetStyle.h";
return true;
default:
break;
}
return false;
}
FString FNewClassInfo::GetBaseClassHeaderFilename() const
{
FString IncludePath;
switch (ClassType)
{
case EClassType::UObject:
if (BaseClass)
{
FString ClassHeaderPath;
if (FSourceCodeNavigation::FindClassHeaderPath(BaseClass, ClassHeaderPath) && IFileManager::Get().FileSize(*ClassHeaderPath) != INDEX_NONE)
{
return ClassHeaderPath;
}
}
break;
case EClassType::SlateWidget:
case EClassType::SlateWidgetStyle:
GetIncludePath(IncludePath);
return FPaths::EngineDir() / TEXT("Source") / TEXT("Runtime") / TEXT("SlateCore") / TEXT("Public") / IncludePath;
default:
return FString();
}
return FString();
}
FString FNewClassInfo::GetHeaderFilename(const FString& ClassName) const
{
const FString HeaderFilename = GetFinalClassName(ClassName) + TEXT(".h");
switch(ClassType)
{
case EClassType::SlateWidget:
return TEXT("S") + HeaderFilename;
default:
break;
}
return HeaderFilename;
}
FString FNewClassInfo::GetSourceFilename(const FString& ClassName) const
{
const FString SourceFilename = GetFinalClassName(ClassName) + TEXT(".cpp");
switch(ClassType)
{
case EClassType::SlateWidget:
return TEXT("S") + SourceFilename;
default:
break;
}
return SourceFilename;
}
FString FNewClassInfo::GetHeaderTemplateFilename() const
{
switch(ClassType)
{
case EClassType::UObject:
{
if (BaseClass != nullptr)
{
if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass()))
{
return TEXT("ActorComponentClass.h.template");
}
else if (BaseClass == AActor::StaticClass())
{
return TEXT("ActorClass.h.template");
}
else if (BaseClass == APawn::StaticClass())
{
return TEXT("PawnClass.h.template");
}
else if (BaseClass == ACharacter::StaticClass())
{
return TEXT("CharacterClass.h.template");
}
// Only check audio-mixer module specific classes if audio mixer is loaded
if (FModuleManager::Get().IsModuleLoaded("AudioMixer"))
{
if (BaseClass == USoundEffectSourcePreset::StaticClass())
{
return TEXT("SoundEffectSourceClass.h.template");
}
else if (BaseClass == USoundEffectSubmixPreset::StaticClass())
{
return TEXT("SoundEffectSubmixClass.h.template");
}
}
}
// Some other non-actor, non-component UObject class
return TEXT( "UObjectClass.h.template" );
}
case EClassType::EmptyCpp:
return TEXT("EmptyClass.h.template");
case EClassType::SlateWidget:
return TEXT("SlateWidget.h.template");
case EClassType::SlateWidgetStyle:
return TEXT("SlateWidgetStyle.h.template");
case EClassType::UInterface:
return TEXT("InterfaceClass.h.template");
default:
break;
}
return TEXT("");
}
FString FNewClassInfo::GetSourceTemplateFilename() const
{
switch(ClassType)
{
case EClassType::UObject:
if (BaseClass != nullptr)
{
if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass()))
{
return TEXT("ActorComponentClass.cpp.template");
}
else if (BaseClass == AActor::StaticClass())
{
return TEXT("ActorClass.cpp.template");
}
else if (BaseClass == APawn::StaticClass())
{
return TEXT("PawnClass.cpp.template");
}
else if (BaseClass == ACharacter::StaticClass())
{
return TEXT("CharacterClass.cpp.template");
}
else if (BaseClass == USoundEffectSubmixPreset::StaticClass())
{
return TEXT("SoundEffectSubmixClass.cpp.template");
}
else if (BaseClass == USoundEffectSourcePreset::StaticClass())
{
return TEXT("SoundEffectSourceClass.cpp.template");
}
}
// Some other non-actor, non-component UObject class
return TEXT( "UObjectClass.cpp.template" );
case EClassType::EmptyCpp:
return TEXT("EmptyClass.cpp.template");
case EClassType::SlateWidget:
return TEXT("SlateWidget.cpp.template");
case EClassType::SlateWidgetStyle:
return TEXT("SlateWidgetStyle.cpp.template");
case EClassType::UInterface:
return TEXT("InterfaceClass.cpp.template");
default:
break;
}
return TEXT("");
}
bool GameProjectUtils::IsValidProjectFileForCreation(const FString& ProjectFile, FText& OutFailReason)
{
const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile);
if ( FPaths::GetPath(ProjectFile).IsEmpty() )
{
OutFailReason = LOCTEXT( "NoProjectPath", "You must specify a path." );
return false;
}
if ( BaseProjectFile.IsEmpty() )
{
OutFailReason = LOCTEXT( "NoProjectName", "You must specify a project name." );
return false;
}
if ( BaseProjectFile.Contains(TEXT(" ")) )
{
OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." );
return false;
}
if ( !FChar::IsAlpha(BaseProjectFile[0]) )
{
OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." );
return false;
}
if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH )
{
FFormatNamedArguments Args;
Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH );
OutFailReason = FText::Format( LOCTEXT( "ProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters." ), Args );
return false;
}
const int32 MaxProjectPathLength = PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE;
if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength )
{
FFormatNamedArguments Args;
Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength );
OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args );
return false;
}
if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() )
{
FFormatNamedArguments Args;
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args );
return false;
}
FString IllegalNameCharacters;
if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args );
return false;
}
if (NameContainsUnderscoreAndXB1Installed(BaseProjectFile))
{
OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." );
return false;
}
if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) )
{
return false;
}
if ( ProjectFileExists(ProjectFile) )
{
OutFailReason = LOCTEXT( "ProjectFileAlreadyExists", "This project file already exists." );
return false;
}
if ( FPaths::ConvertRelativePathToFull(FPaths::GetPath(ProjectFile)).StartsWith( FPaths::ConvertRelativePathToFull(FPaths::EngineDir())) )
{
OutFailReason = LOCTEXT( "ProjectFileCannotBeUnderEngineFolder", "Project cannot be saved under the Engine folder. Please choose a different directory." );
return false;
}
if ( AnyProjectFilesExistInFolder(FPaths::GetPath(ProjectFile)) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
OutFailReason = FText::Format( LOCTEXT( "AProjectFileAlreadyExistsAtLoction", "Another .{ProjectFileExtension} file already exists in the specified folder" ), Args );
return false;
}
// Don't allow any files within target directory so we can safely delete everything on failure
TArray<FString> ExistingFiles;
IFileManager::Get().FindFiles(ExistingFiles, *(FPaths::GetPath(ProjectFile) / TEXT("*")), true, true);
if (ExistingFiles.Num() > 0)
{
OutFailReason = LOCTEXT("ProjectFileCannotBeWithExistingFiles", "Project cannot be saved in a folder with existing files. Please choose a different directory/project name.");
return false;
}
return true;
}
bool GameProjectUtils::OpenProject(const FString& ProjectFile, FText& OutFailReason)
{
if ( ProjectFile.IsEmpty() )
{
OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." );
return false;
}
const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile);
if ( BaseProjectFile.Contains(TEXT(" ")) )
{
OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." );
return false;
}
if ( !FChar::IsAlpha(BaseProjectFile[0]) )
{
OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." );
return false;
}
const int32 MaxProjectPathLength = PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE;
if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength )
{
FFormatNamedArguments Args;
Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength );
OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args );
return false;
}
if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() )
{
FFormatNamedArguments Args;
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args );
return false;
}
FString IllegalNameCharacters;
if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args );
return false;
}
if (NameContainsUnderscoreAndXB1Installed(BaseProjectFile))
{
OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." );
return false;
}
if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) )
{
return false;
}
if ( !ProjectFileExists(ProjectFile) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("ProjectFile"), FText::FromString( ProjectFile ) );
OutFailReason = FText::Format( LOCTEXT( "ProjectFileDoesNotExist", "{ProjectFile} does not exist." ), Args );
return false;
}
FUnrealEdMisc::Get().SwitchProject(ProjectFile, false);
return true;
}
bool GameProjectUtils::OpenCodeIDE(const FString& ProjectFile, FText& OutFailReason)
{
if ( ProjectFile.IsEmpty() )
{
OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." );
return false;
}
// Check whether this project is a foreign project. Don't use the cached project dictionary; we may have just created a new project.
FString SolutionFolder;
FString SolutionFilenameWithoutExtension;
if( FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFile) )
{
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(ProjectFile));
SolutionFilenameWithoutExtension = FPaths::GetBaseFilename(ProjectFile);
}
else
{
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir());
SolutionFilenameWithoutExtension = TEXT("UE4");
}
// Get the solution filename
FString CodeSolutionFile;
#if PLATFORM_WINDOWS
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".sln");
#elif PLATFORM_MAC
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".xcworkspace");
#elif PLATFORM_LINUX
// FIXME: Should depend on PreferredAccessor setting
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".workspace");
#else
OutFailReason = LOCTEXT( "OpenCodeIDE_UnknownPlatform", "could not open the code editing IDE. The operating system is unknown." );
return false;
#endif
// Open the solution with the default application
const FString FullPath = FPaths::Combine(*SolutionFolder, *CodeSolutionFile);
#if PLATFORM_MAC
if ( IFileManager::Get().DirectoryExists(*FullPath) )
#else
if ( FPaths::FileExists(FullPath) )
#endif
{
FPlatformProcess::LaunchFileInDefaultExternalApplication( *FullPath );
return true;
}
else
{
FFormatNamedArguments Args;
Args.Add( TEXT("Path"), FText::FromString( FullPath ) );
OutFailReason = FText::Format( LOCTEXT( "OpenCodeIDE_MissingFile", "Could not edit the code editing IDE. {Path} could not be found." ), Args );
return false;
}
}
void GameProjectUtils::GetStarterContentFiles(TArray<FString>& OutFilenames)
{
FString const SrcFolder = FPaths::FeaturePackDir();
FString SearchPath = TEXT("*");
SearchPath += DefaultFeaturePackExtension;
IFileManager::Get().FindFilesRecursive(OutFilenames, *SrcFolder, *SearchPath, /*Files=*/true, /*Directories=*/false);
}
bool GameProjectUtils::CreateProject(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray<FString>* OutCreatedFiles)
{
if ( !IsValidProjectFileForCreation(InProjectInfo.ProjectFilename, OutFailReason) )
{
return false;
}
FScopedSlowTask SlowTask(0, LOCTEXT( "CreatingProjectStatus", "Creating project..." ));
SlowTask.MakeDialog();
bool bProjectCreationSuccessful = false;
FString TemplateName;
if ( InProjectInfo.TemplateFile.IsEmpty() )
{
bProjectCreationSuccessful = GenerateProjectFromScratch(InProjectInfo, OutFailReason, OutFailLog);
TemplateName = InProjectInfo.bShouldGenerateCode ? TEXT("Basic Code") : TEXT("Blank");
}
else
{
bProjectCreationSuccessful = CreateProjectFromTemplate(InProjectInfo, OutFailReason, OutFailLog, OutCreatedFiles);
TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
}
if (!bProjectCreationSuccessful && CleanupIsEnabled())
{
// Delete the new project folder
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
IFileManager::Get().DeleteDirectory(*NewProjectFolder, /*RequireExists=*/false, /*Tree=*/true);
if( OutCreatedFiles != nullptr )
{
OutCreatedFiles->Empty();
}
}
if( FEngineAnalytics::IsAvailable() )
{
TArray<FAnalyticsEventAttribute> EventAttributes;
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Template"), TemplateName));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ProjectType"), InProjectInfo.bShouldGenerateCode ? TEXT("C++ Code") : TEXT("Content Only")));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), bProjectCreationSuccessful ? TEXT("Successful") : TEXT("Failed")));
UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EHardwareClass"), true);
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HardwareClass"), Enum ? Enum->GetEnumName(InProjectInfo.TargetedHardware) : FString()));
Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EGraphicsPreset"), true);
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("GraphicsPreset"), Enum ? Enum->GetEnumName(InProjectInfo.DefaultGraphicsPerformance) : FString()));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("StarterContent"), InProjectInfo.bCopyStarterContent ? TEXT("Yes") : TEXT("No")));
FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.NewProject.ProjectCreated" ), EventAttributes );
}
return bProjectCreationSuccessful;
}
void GameProjectUtils::CheckForOutOfDateGameProjectFile()
{
if ( FPaths::IsProjectFilePathSet() )
{
if (IProjectManager::Get().IsCurrentProjectDirty())
{
FText FailMessage;
TryMakeProjectFileWriteable(FPaths::GetProjectFilePath());
if (!IProjectManager::Get().SaveCurrentProjectToDisk(FailMessage))
{
FMessageDialog::Open(EAppMsgType::Ok, FailMessage);
}
}
FProjectStatus ProjectStatus;
if (IProjectManager::Get().QueryStatusForCurrentProject(ProjectStatus))
{
if ( ProjectStatus.bRequiresUpdate )
{
const FText UpdateProjectText = LOCTEXT("UpdateProjectFilePrompt", "Project file is saved in an older format. Would you like to update it?");
const FText UpdateProjectConfirmText = LOCTEXT("UpdateProjectFileConfirm", "Update");
const FText UpdateProjectCancelText = LOCTEXT("UpdateProjectFileCancel", "Not Now");
FNotificationInfo Info(UpdateProjectText);
Info.bFireAndForget = false;
Info.bUseLargeFont = false;
Info.bUseThrobber = false;
Info.bUseSuccessFailIcons = false;
Info.FadeOutDuration = 3.f;
Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectConfirmText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectConfirm)));
Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectCancelText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectCancel)));
if (UpdateGameProjectNotification.IsValid())
{
UpdateGameProjectNotification.Pin()->ExpireAndFadeout();
UpdateGameProjectNotification.Reset();
}
UpdateGameProjectNotification = FSlateNotificationManager::Get().AddNotification(Info);
if (UpdateGameProjectNotification.IsValid())
{
UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
}
}
}
// Check if there are any installed plugins which aren't referenced by the project file
if(!UpdateGameProjectNotification.IsValid())
{
const FProjectDescriptor* Project = IProjectManager::Get().GetCurrentProject();
if(Project != nullptr)
{
TArray<FPluginReferenceDescriptor> NewPluginReferences;
for(TSharedRef<IPlugin>& Plugin: IPluginManager::Get().GetEnabledPlugins())
{
if(Plugin->GetDescriptor().bInstalled && Project->FindPluginReferenceIndex(Plugin->GetName()) == INDEX_NONE)
{
FPluginReferenceDescriptor PluginReference(Plugin->GetName(), true, Plugin->GetDescriptor().MarketplaceURL);
NewPluginReferences.Add(PluginReference);
}
}
if(NewPluginReferences.Num() > 0)
{
UpdateProject(FProjectDescriptorModifier::CreateLambda(
[NewPluginReferences](FProjectDescriptor& Descriptor){ Descriptor.Plugins.Append(NewPluginReferences); return true; }
));
}
}
}
}
}
void GameProjectUtils::CheckAndWarnProjectFilenameValid()
{
const FString& LoadedProjectFilePath = FPaths::IsProjectFilePathSet() ? FPaths::GetProjectFilePath() : FString();
if ( !LoadedProjectFilePath.IsEmpty() )
{
const FString BaseProjectFile = FPaths::GetBaseFilename(LoadedProjectFilePath);
if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH )
{
FFormatNamedArguments Args;
Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH );
const FText WarningReason = FText::Format( LOCTEXT( "WarnProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters.\nYou might have problems saving or modifying a project with a longer name." ), Args );
const FText WarningReasonOkText = LOCTEXT("WarningReasonOkText", "Ok");
FNotificationInfo Info(WarningReason);
Info.bFireAndForget = false;
Info.bUseLargeFont = false;
Info.bUseThrobber = false;
Info.bUseSuccessFailIcons = false;
Info.FadeOutDuration = 3.f;
Info.ButtonDetails.Add(FNotificationButtonInfo(WarningReasonOkText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnWarningReasonOk)));
if (WarningProjectNameNotification.IsValid())
{
WarningProjectNameNotification.Pin()->ExpireAndFadeout();
WarningProjectNameNotification.Reset();
}
WarningProjectNameNotification = FSlateNotificationManager::Get().AddNotification(Info);
if (WarningProjectNameNotification.IsValid())
{
WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
}
}
}
}
void GameProjectUtils::OnWarningReasonOk()
{
if ( WarningProjectNameNotification.IsValid() )
{
WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_None);
WarningProjectNameNotification.Pin()->ExpireAndFadeout();
WarningProjectNameNotification.Reset();
}
}
bool GameProjectUtils::UpdateStartupModuleNames(FProjectDescriptor& Descriptor, const TArray<FString>* StartupModuleNames)
{
if (StartupModuleNames == nullptr)
{
return false;
}
// Replace the modules names, if specified
Descriptor.Modules.Empty();
for (int32 Idx = 0; Idx < StartupModuleNames->Num(); Idx++)
{
Descriptor.Modules.Add(FModuleDescriptor(*(*StartupModuleNames)[Idx]));
}
return true;
}
bool GameProjectUtils::UpdateRequiredAdditionalDependencies(FProjectDescriptor& Descriptor, TArray<FString>& RequiredDependencies, const FString& ModuleName)
{
bool bNeedsUpdate = false;
for (auto& ModuleDesc : Descriptor.Modules)
{
if (ModuleDesc.Name != *ModuleName)
{
continue;
}
for (const auto& RequiredDep : RequiredDependencies)
{
if (!ModuleDesc.AdditionalDependencies.Contains(RequiredDep))
{
ModuleDesc.AdditionalDependencies.Add(RequiredDep);
bNeedsUpdate = true;
}
}
}
return bNeedsUpdate;
}
bool GameProjectUtils::UpdateGameProject(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason)
{
return UpdateGameProjectFile(ProjectFile, EngineIdentifier, OutFailReason);
}
void GameProjectUtils::OpenAddToProjectDialog(const FAddToProjectConfig& Config, EClassDomain InDomain)
{
// If we've been given a class then we only show the second page of the dialog, so we can make the window smaller as that page doesn't have as much content
const FVector2D WindowSize = (Config._ParentClass) ? (InDomain == EClassDomain::Blueprint) ? FVector2D(940, 480) : FVector2D(940, 380) : FVector2D(940, 540);
FText WindowTitle = Config._WindowTitle;
if (WindowTitle.IsEmpty())
{
WindowTitle = InDomain == EClassDomain::Native ? LOCTEXT("AddCodeWindowHeader_Native", "Add C++ Class") : LOCTEXT("AddCodeWindowHeader_Blueprint", "Add Blueprint Class");
}
TSharedRef<SWindow> AddCodeWindow =
SNew(SWindow)
.Title( WindowTitle )
.ClientSize( WindowSize )
.SizingRule( ESizingRule::FixedSize )
.SupportsMinimize(false) .SupportsMaximize(false);
TSharedRef<SNewClassDialog> NewClassDialog =
SNew(SNewClassDialog)
.Class(Config._ParentClass)
.ClassViewerFilter(Config._AllowableParents)
.ClassDomain(InDomain)
.FeaturedClasses(Config._FeaturedClasses)
.InitialPath(Config._InitialPath)
.OnAddedToProject( Config._OnAddedToProject )
.DefaultClassPrefix( Config._DefaultClassPrefix )
.DefaultClassName( Config._DefaultClassName );
AddCodeWindow->SetContent( NewClassDialog );
TSharedPtr<SWindow> ParentWindow = Config._ParentWindow;
if (!ParentWindow.IsValid())
{
static const FName MainFrameModuleName = "MainFrame";
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(MainFrameModuleName);
ParentWindow = MainFrameModule.GetParentWindow();
}
if (Config._bModal)
{
FSlateApplication::Get().AddModalWindow(AddCodeWindow, ParentWindow);
}
else if (ParentWindow.IsValid())
{
FSlateApplication::Get().AddWindowAsNativeChild(AddCodeWindow, ParentWindow.ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(AddCodeWindow);
}
}
bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, FText& OutFailReason)
{
if ( NewClassName.IsEmpty() )
{
OutFailReason = LOCTEXT( "NoClassName", "You must specify a class name." );
return false;
}
if ( NewClassName.Contains(TEXT(" ")) )
{
OutFailReason = LOCTEXT( "ClassNameContainsSpace", "Your class name may not contain a space." );
return false;
}
if ( !FChar::IsAlpha(NewClassName[0]) )
{
OutFailReason = LOCTEXT( "ClassNameMustBeginWithACharacter", "Your class name must begin with an alphabetic character." );
return false;
}
if ( NewClassName.Len() > MAX_CLASS_NAME_LENGTH )
{
OutFailReason = FText::Format( LOCTEXT( "ClassNameTooLong", "The class name must not be longer than {0} characters." ), FText::AsNumber(MAX_CLASS_NAME_LENGTH) );
return false;
}
FString IllegalNameCharacters;
if ( !NameContainsOnlyLegalCharacters(NewClassName, IllegalNameCharacters) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
OutFailReason = FText::Format( LOCTEXT( "ClassNameContainsIllegalCharacters", "The class name may not contain the following characters: {IllegalNameCharacters}" ), Args );
return false;
}
return true;
}
bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, const FModuleContextInfo& ModuleInfo, const TSet<FString>& DisallowedHeaderNames, FText& OutFailReason)
{
if (!IsValidClassNameForCreation(NewClassName, OutFailReason))
{
return false;
}
// Look for a duplicate class in memory
for ( TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt )
{
if ( ClassIt->GetName() == NewClassName )
{
FFormatNamedArguments Args;
Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) );
OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args );
return false;
}
}
// Look for a duplicate class on disk in their project
{
FString UnusedFoundPath;
if ( FindSourceFileInProject(NewClassName + ".h", ModuleInfo.ModuleSourcePath, UnusedFoundPath) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) );
OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args );
return false;
}
}
// See if header name clashes with an engine header
{
FString UnusedFoundPath;
if (DisallowedHeaderNames.Contains(NewClassName))
{
FFormatNamedArguments Args;
Args.Add(TEXT("NewHeaderName"), FText::FromString(NewClassName + ".h"));
OutFailReason = FText::Format(LOCTEXT("HeaderNameAlreadyExists", "The file {NewHeaderName} already exists elsewhere in the engine."), Args);
return false;
}
}
return true;
}
bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const FModuleContextInfo& InModuleInfo)
{
auto DoesClassNeedAPIExport = [&InModuleInfo](const FString& InClassModuleName) -> bool
{
return InModuleInfo.ModuleName != InClassModuleName;
};
return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport));
}
bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const TArray<FModuleContextInfo>& InModuleInfoArray)
{
auto DoesClassNeedAPIExport = [&InModuleInfoArray](const FString& InClassModuleName) -> bool
{
for(const FModuleContextInfo& ModuleInfo : InModuleInfoArray)
{
if(ModuleInfo.ModuleName == InClassModuleName)
{
return false;
}
}
return true;
};
return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport));
}
bool GameProjectUtils::IsValidBaseClassForCreation_Internal(const UClass* InClass, const FDoesClassNeedAPIExportCallback& InDoesClassNeedAPIExport)
{
// You may not make native classes based on blueprint generated classes
const bool bIsBlueprintClass = (InClass->ClassGeneratedBy != nullptr);
// UObject is special cased to be extensible since it would otherwise not be since it doesn't pass the API check (intrinsic class).
const bool bIsExplicitlyUObject = (InClass == UObject::StaticClass());
// You need API if you are not UObject itself, and you're in a module that was validated as needing API export
const FString ClassModuleName = InClass->GetOutermost()->GetName().RightChop( FString(TEXT("/Script/")).Len() );
const bool bNeedsAPI = !bIsExplicitlyUObject && InDoesClassNeedAPIExport.Execute(ClassModuleName);
// You may not make a class that is not DLL exported.
// MinimalAPI classes aren't compatible with the DLL export macro, but can still be used as a valid base
const bool bHasAPI = InClass->HasAnyClassFlags(CLASS_RequiredAPI) || InClass->HasAnyClassFlags(CLASS_MinimalAPI);
// @todo should we support interfaces?
const bool bIsInterface = InClass->IsChildOf(UInterface::StaticClass());
return !bIsBlueprintClass && (!bNeedsAPI || bHasAPI) && !bIsInterface;
}
GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet<FString>& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason)
{
const EAddCodeToProjectResult Result = AddCodeToProject_Internal(NewClassName, NewClassPath, ModuleInfo, ParentClassInfo, DisallowedHeaderNames, OutHeaderFilePath, OutCppFilePath, OutFailReason);
if( FEngineAnalytics::IsAvailable() )
{
const FString ParentClassName = ParentClassInfo.GetClassNameCPP();
TArray<FAnalyticsEventAttribute> EventAttributes;
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ParentClass"), ParentClassName.IsEmpty() ? TEXT("None") : ParentClassName));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), Result == EAddCodeToProjectResult::Succeeded ? TEXT("Successful") : TEXT("Failed")));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("FailureReason"), OutFailReason.ToString()));
FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.AddCodeToProject.CodeAdded" ), EventAttributes );
}
return Result;
}
UTemplateProjectDefs* GameProjectUtils::LoadTemplateDefs(const FString& ProjectDirectory)
{
UTemplateProjectDefs* TemplateDefs = NULL;
const FString TemplateDefsIniFilename = ProjectDirectory / TEXT("Config") / GetTemplateDefsFilename();
if ( FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateDefsIniFilename) )
{
UClass* ClassToConstruct = UDefaultTemplateProjectDefs::StaticClass();
// see if template uses a custom project defs object
FString ClassName;
const bool bFoundValue = GConfig->GetString(*UTemplateProjectDefs::StaticClass()->GetPathName(), TEXT("TemplateProjectDefsClass"), ClassName, TemplateDefsIniFilename);
if (bFoundValue && ClassName.Len() > 0)
{
UClass* OverrideClass = FindObject<UClass>(ANY_PACKAGE, *ClassName, false);
if (nullptr != OverrideClass)
{
ClassToConstruct = OverrideClass;
}
else
{
UE_LOG(LogGameProjectGeneration, Error, TEXT("Failed to find template project defs class '%s', using default."), *ClassName);
}
}
TemplateDefs = NewObject<UTemplateProjectDefs>(GetTransientPackage(), ClassToConstruct);
TemplateDefs->LoadConfig(UTemplateProjectDefs::StaticClass(), *TemplateDefsIniFilename);
}
return TemplateDefs;
}
bool GameProjectUtils::GenerateProjectFromScratch(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog)
{
FScopedSlowTask SlowTask(5);
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
TArray<FString> CreatedFiles;
SlowTask.EnterProgressFrame();
// Generate config files
if (!GenerateConfigFiles(InProjectInfo, CreatedFiles, OutFailReason))
{
return false;
}
// Insert any required feature packs (EG starter content) into ini file. These will be imported automatically when the editor is first run
if(!InsertFeaturePacksIntoINIFile(InProjectInfo, OutFailReason))
{
return false;
}
// Make the Content folder
const FString ContentFolder = NewProjectFolder / TEXT("Content");
if ( !IFileManager::Get().MakeDirectory(*ContentFolder) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("ContentFolder"), FText::FromString( ContentFolder ) );
OutFailReason = FText::Format( LOCTEXT("FailedToCreateContentFolder", "Failed to create the content folder {ContentFolder}"), Args );
return false;
}
SlowTask.EnterProgressFrame();
TArray<FString> StartupModuleNames;
if ( InProjectInfo.bShouldGenerateCode )
{
FScopedSlowTask LocalScope(2);
LocalScope.EnterProgressFrame();
// Generate basic source code files
if ( !GenerateBasicSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, NewProjectFolder, StartupModuleNames, CreatedFiles, OutFailReason) )
{
return false;
}
LocalScope.EnterProgressFrame();
// Generate game framework source code files
if ( !GenerateGameFrameworkSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, CreatedFiles, OutFailReason) )
{
return false;
}
}
SlowTask.EnterProgressFrame();
// Generate the project file
{
// Set up the descriptor
FProjectDescriptor Descriptor;
for(int32 Idx = 0; Idx < StartupModuleNames.Num(); Idx++)
{
Descriptor.Modules.Add(FModuleDescriptor(*StartupModuleNames[Idx]));
}
// Try to save it
FText LocalFailReason;
if(!Descriptor.Save(InProjectInfo.ProjectFilename, LocalFailReason))
{
OutFailReason = LocalFailReason;
return false;
}
CreatedFiles.Add(InProjectInfo.ProjectFilename);
// Set the engine identifier for it. Do this after saving, so it can be correctly detected as foreign or non-foreign.
if(!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason))
{
return false;
}
}
SlowTask.EnterProgressFrame();
if ( InProjectInfo.bShouldGenerateCode )
{
// Generate project files
if ( !GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog) )
{
return false;
}
}
SlowTask.EnterProgressFrame();
UE_LOG(LogGameProjectGeneration, Log, TEXT("Created new project with %d files (plus project files)"), CreatedFiles.Num());
return true;
}
bool GameProjectUtils::CreateProjectFromTemplate(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray<FString>* OutCreatedFiles)
{
FScopedSlowTask SlowTask(10);
const FString ProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile);
const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
if ( !FPlatformFileManager::Get().GetPlatformFile().FileExists(*InProjectInfo.TemplateFile) )
{
FFormatNamedArguments Args;
Args.Add( TEXT("TemplateFile"), FText::FromString( InProjectInfo.TemplateFile ) );
OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingProject", "Template project \"{TemplateFile}\" does not exist."), Args );
return false;
}
SlowTask.EnterProgressFrame();
UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder);
if ( TemplateDefs == NULL )
{
FFormatNamedArguments Args;
Args.Add( TEXT("TemplateFile"), FText::FromString( FPaths::GetBaseFilename(InProjectInfo.TemplateFile) ) );
Args.Add( TEXT("TemplateDefinesFile"), FText::FromString( GetTemplateDefsFilename() ) );
OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingDefs", "Template project \"{TemplateFile}\" does not have definitions file: '{TemplateDefinesFile}'."), Args );
return false;
}
SlowTask.EnterProgressFrame();
// Fix up the replacement strings using the specified project name
TemplateDefs->FixupStrings(TemplateName, ProjectName);
// Form a list of all extensions we care about
TSet<FString> ReplacementsInFilesExtensions;
for ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
{
ReplacementsInFilesExtensions.Append((*ReplacementIt).Extensions);
}
// Keep a list of created files so we can delete them if project creation fails
TArray<FString> CreatedFiles;
SlowTask.EnterProgressFrame();
// Discover and copy all files in the src folder to the destination, excluding a few files and folders
TArray<FString> FilesToCopy;
TArray<FString> FilesThatNeedContentsReplaced;
TMap<FString, FString> ClassRenames;
IFileManager::Get().FindFilesRecursive(FilesToCopy, *SrcFolder, TEXT("*"), /*Files=*/true, /*Directories=*/false);
SlowTask.EnterProgressFrame();
{
// Open a new feedback scope for the loop so we can report how far through the copy we are
FScopedSlowTask InnerSlowTask(FilesToCopy.Num());
for ( auto FileIt = FilesToCopy.CreateConstIterator(); FileIt; ++FileIt )
{
const FString SrcFilename = (*FileIt);
// Update the progress
FFormatNamedArguments Args;
Args.Add( TEXT("SrcFilename"), FText::FromString( FPaths::GetCleanFilename(SrcFilename) ) );
InnerSlowTask.EnterProgressFrame(1, FText::Format( LOCTEXT( "CreatingProjectStatus_CopyingFile", "Copying File {SrcFilename}..." ), Args ));
// Get the file path, relative to the src folder
const FString SrcFileSubpath = SrcFilename.RightChop(SrcFolder.Len() + 1);
// Skip any files that were configured to be ignored
bool bThisFileIsIgnored = false;
for ( auto IgnoreIt = TemplateDefs->FilesToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt )
{
if ( SrcFileSubpath == *IgnoreIt )
{
// This file was marked as "ignored"
bThisFileIsIgnored = true;
break;
}
}
if ( bThisFileIsIgnored )
{
// This file was marked as "ignored"
continue;
}
// Skip any folders that were configured to be ignored
bool bThisFolderIsIgnored = false;
for ( auto IgnoreIt = TemplateDefs->FoldersToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt )
{
if ( SrcFileSubpath.StartsWith((*IgnoreIt) + TEXT("/") ) )
{
// This folder was marked as "ignored"
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Skipping as it is in an ignored folder '%s'"), *SrcFilename, **IgnoreIt);
bThisFolderIsIgnored = true;
break;
}
}
if ( bThisFolderIsIgnored )
{
// This folder was marked as "ignored"
continue;
}
// Retarget any folders that were chosen to be renamed by choosing a new destination subpath now
FString DestFileSubpathWithoutFilename = FPaths::GetPath(SrcFileSubpath) + TEXT("/");
for ( auto RenameIt = TemplateDefs->FolderRenames.CreateConstIterator(); RenameIt; ++RenameIt )
{
const FTemplateFolderRename& FolderRename = *RenameIt;
if ( SrcFileSubpath.StartsWith(FolderRename.From + TEXT("/")) )
{
// This was a file in a renamed folder. Retarget to the new location
DestFileSubpathWithoutFilename = FolderRename.To / DestFileSubpathWithoutFilename.RightChop( FolderRename.From.Len() );
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Moving to '%s' as it matched folder rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *FolderRename.From, *FolderRename.To);
}
}
// Retarget any files that were chosen to have parts of their names replaced here
FString DestBaseFilename = FPaths::GetBaseFilename(SrcFileSubpath);
const FString FileExtension = FPaths::GetExtension(SrcFileSubpath);
for ( auto ReplacementIt = TemplateDefs->FilenameReplacements.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
{
const FTemplateReplacement& Replacement = *ReplacementIt;
if ( Replacement.Extensions.Contains( FileExtension ) )
{
// This file matched a filename replacement extension, apply it now
FString LastDestBaseFilename = DestBaseFilename;
DestBaseFilename = DestBaseFilename.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);
if (LastDestBaseFilename != DestBaseFilename)
{
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Renaming to '%s/%s' as it matched file rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *DestBaseFilename, *Replacement.From, *Replacement.To);
}
}
}
// Perform the copy
const FString DestFilename = DestFolder / DestFileSubpathWithoutFilename + DestBaseFilename + TEXT(".") + FileExtension;
if ( IFileManager::Get().Copy(*DestFilename, *SrcFilename) == COPY_OK )
{
CreatedFiles.Add(DestFilename);
if ( ReplacementsInFilesExtensions.Contains(FileExtension) )
{
FilesThatNeedContentsReplaced.Add(DestFilename);
}
// Allow project template to extract class renames from this file copy
if (FPaths::GetBaseFilename(SrcFilename) != FPaths::GetBaseFilename(DestFilename)
&& TemplateDefs->IsClassRename(DestFilename, SrcFilename, FileExtension))
{
// Looks like a UObject file!
ClassRenames.Add(FPaths::GetBaseFilename(SrcFilename), FPaths::GetBaseFilename(DestFilename));
}
}
else
{
FFormatNamedArguments FailArgs;
FailArgs.Add(TEXT("SrcFilename"), FText::FromString(SrcFilename));
FailArgs.Add(TEXT("DestFilename"), FText::FromString(DestFilename));
OutFailReason = FText::Format(LOCTEXT("FailedToCopyFile", "Failed to copy \"{SrcFilename}\" to \"{DestFilename}\"."), FailArgs);
return false;
}
}
}
SlowTask.EnterProgressFrame();
{
// Open a new feedback scope for the loop so we can report how far through the process we are
FScopedSlowTask InnerSlowTask(FilesThatNeedContentsReplaced.Num());
// Open all files with the specified extensions and replace text
for ( auto FileIt = FilesThatNeedContentsReplaced.CreateConstIterator(); FileIt; ++FileIt )
{
InnerSlowTask.EnterProgressFrame();
const FString FileToFix = *FileIt;
bool bSuccessfullyProcessed = false;
FString FileContents;
if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) )
{
for ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
{
const FTemplateReplacement& Replacement = *ReplacementIt;
if ( Replacement.Extensions.Contains( FPaths::GetExtension(FileToFix) ) )
{
FileContents = FileContents.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);
}
}
if ( FFileHelper::SaveStringToFile(FileContents, *FileToFix) )
{
bSuccessfullyProcessed = true;
}
}
if ( !bSuccessfullyProcessed )
{
FFormatNamedArguments Args;
Args.Add( TEXT("FileToFix"), FText::FromString( FileToFix ) );
OutFailReason = FText::Format( LOCTEXT("FailedToFixUpFile", "Failed to process file \"{FileToFix}\"."), Args );
return false;
}
}
}
SlowTask.EnterProgressFrame();
const FString ProjectConfigPath = DestFolder / TEXT("Config");
// Write out the hardware class target settings chosen for this project
{
const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini");
FString FileContents;
// Load the existing file - if it doesn't exist we create it
FFileHelper::LoadFileToString(FileContents, *DefaultEngineIniFilename);
FileContents += LINE_TERMINATOR;
FileContents += GetHardwareConfigString(InProjectInfo);
if ( !WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason) )
{
return false;
}
}
// Fixup specific ini values
TArray<FTemplateConfigValue> ConfigValuesToSet;
TemplateDefs->AddConfigValues(ConfigValuesToSet, TemplateName, ProjectName, InProjectInfo.bShouldGenerateCode);
new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), FGuid::NewGuid().ToString(), /*InShouldReplaceExistingValue=*/true);
// Add all classname fixups
for ( auto RenameIt = ClassRenames.CreateConstIterator(); RenameIt; ++RenameIt )
{
const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *RenameIt.Key(), *RenameIt.Value());
new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false);
}
// Fix all specified config values
for ( auto ConfigIt = ConfigValuesToSet.CreateConstIterator(); ConfigIt; ++ConfigIt )
{
const FTemplateConfigValue& ConfigValue = *ConfigIt;
const FString IniFilename = ProjectConfigPath / ConfigValue.ConfigFile;
bool bSuccessfullyProcessed = false;
TArray<FString> FileLines;
if ( FFileHelper::LoadANSITextFileToStrings(*IniFilename, &IFileManager::Get(), FileLines) )
{
FString FileOutput;
const FString TargetSection = ConfigValue.ConfigSection;
FString CurSection;
bool bFoundTargetKey = false;
for ( auto LineIt = FileLines.CreateConstIterator(); LineIt; ++LineIt )
{
FString Line = *LineIt;
Line.Trim().TrimTrailing();
bool bShouldExcludeLineFromOutput = false;
// If we not yet found the target key parse each line looking for it
if ( !bFoundTargetKey )
{
// Check for an empty line. No work needs to be done on these lines
if ( Line.Len() == 0 )
{
}
// Comment lines start with ";". Skip these lines entirely.
else if ( Line.StartsWith(TEXT(";")) )
{
}
// If this is a section line, update the section
else if ( Line.StartsWith(TEXT("[")) )
{
// If we are entering a new section and we have not yet found our key in the target section, add it to the end of the section
if ( CurSection == TargetSection )
{
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR + LINE_TERMINATOR;
bFoundTargetKey = true;
}
// Update the current section
CurSection = Line.Mid(1, Line.Len() - 2);
}
// This is possibly an actual key/value pair
else if ( CurSection == TargetSection )
{
// Key value pairs contain an equals sign
const int32 EqualsIdx = Line.Find(TEXT("="));
if ( EqualsIdx != INDEX_NONE )
{
// Determine the key and see if it is the target key
const FString Key = Line.Left(EqualsIdx);
if ( Key == ConfigValue.ConfigKey )
{
// Found the target key, add it to the output and skip the current line if the target value is supposed to replace
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR;
bShouldExcludeLineFromOutput = ConfigValue.bShouldReplaceExistingValue;
bFoundTargetKey = true;
}
}
}
}
// Unless we replaced the key, add this line to the output
if ( !bShouldExcludeLineFromOutput )
{
FileOutput += Line;
if ( LineIt.GetIndex() < FileLines.Num() - 1 )
{
// Add a line terminator on every line except the last
FileOutput += LINE_TERMINATOR;
}
}
}
// If the key did not exist, add it here
if ( !bFoundTargetKey )
{
// If we did not end in the correct section, add the section to the bottom of the file
if ( CurSection != TargetSection )
{
FileOutput += LINE_TERMINATOR;
FileOutput += LINE_TERMINATOR;
FileOutput += FString::Printf(TEXT("[%s]"), *TargetSection) + LINE_TERMINATOR;
}
// Add the key/value here
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR;
}
if ( FFileHelper::SaveStringToFile(FileOutput, *IniFilename) )
{
bSuccessfullyProcessed = true;
}
}
if ( !bSuccessfullyProcessed )
{
OutFailReason = LOCTEXT("FailedToFixUpDefaultEngine", "Failed to process file DefaultEngine.ini");
return false;
}
}
// Insert any required feature packs (EG starter content) into ini file. These will be imported automatically when the editor is first run
if ( !InsertFeaturePacksIntoINIFile(InProjectInfo, OutFailReason) )
{
return false;
}
if ( !AddSharedContentToProject(InProjectInfo, CreatedFiles, OutFailReason) )
{
return false;
}
SlowTask.EnterProgressFrame();
// Generate the project file
{
// Load the source project
FProjectDescriptor Project;
if ( !Project.Load(InProjectInfo.TemplateFile, OutFailReason) )
{
return false;
}
// Update it to current
Project.EngineAssociation.Empty();
Project.EpicSampleNameHash = 0;
// Fix up module names
const FString BaseSourceName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
const FString BaseNewName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
for ( auto ModuleIt = Project.Modules.CreateIterator(); ModuleIt; ++ModuleIt )
{
FModuleDescriptor& ModuleInfo = *ModuleIt;
ModuleInfo.Name = FName(*ModuleInfo.Name.ToString().Replace(*BaseSourceName, *BaseNewName));
}
// Save it to disk
if(!Project.Save(InProjectInfo.ProjectFilename, OutFailReason))
{
return false;
}
// Set the engine identifier if it's a foreign project. Do this after saving, so it can be correctly detected as foreign.
if(!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason))
{
return false;
}
// Add it to the list of created files
CreatedFiles.Add(InProjectInfo.ProjectFilename);
}
SlowTask.EnterProgressFrame();
SlowTask.EnterProgressFrame();
if ( InProjectInfo.bShouldGenerateCode )
{
// Generate project files
if ( !GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog) )
{
return false;
}
}
SlowTask.EnterProgressFrame();
if (!TemplateDefs->PostGenerateProject(DestFolder, SrcFolder, InProjectInfo.ProjectFilename, InProjectInfo.TemplateFile, InProjectInfo.bShouldGenerateCode, OutFailReason))
{
return false;
}
if( OutCreatedFiles != nullptr )
{
OutCreatedFiles->Append(CreatedFiles);
}
return true;
}
bool GameProjectUtils::SetEngineAssociationForForeignProject(const FString& ProjectFileName, FText& OutFailReason)
{
if(FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFileName))
{
if(!FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier()))
{
OutFailReason = LOCTEXT("FailedToSetEngineIdentifier", "Couldn't set engine identifier for project");
return false;
}
}
return true;
}
FString GameProjectUtils::GetTemplateDefsFilename()
{
return TEXT("TemplateDefs.ini");
}
bool GameProjectUtils::NameContainsOnlyLegalCharacters(const FString& TestName, FString& OutIllegalCharacters)
{
bool bContainsIllegalCharacters = false;
// Only allow alphanumeric characters in the project name
bool bFoundAlphaNumericChar = false;
for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx )
{
const FString& Char = TestName.Mid( CharIdx, 1 );
if ( !FChar::IsAlnum(Char[0]) && Char != TEXT("_") )
{
if ( !OutIllegalCharacters.Contains( Char ) )
{
OutIllegalCharacters += Char;
}
bContainsIllegalCharacters = true;
}
}
return !bContainsIllegalCharacters;
}
bool GameProjectUtils::NameContainsUnderscoreAndXB1Installed(const FString& TestName)
{
// disabled for now so people with the SDK installed can use the editor
return false;
bool bContainsIllegalCharacters = false;
// Only allow alphanumeric characters in the project name
for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx )
{
const FString& Char = TestName.Mid( CharIdx, 1 );
if ( Char == TEXT("_") )
{
const ITargetPlatform* Platform = GetTargetPlatformManager()->FindTargetPlatform(TEXT("XboxOne"));
if (Platform)
{
FString NotInstalledDocLink;
if (Platform->IsSdkInstalled(true, NotInstalledDocLink))
{
bContainsIllegalCharacters = true;
}
}
}
}
return bContainsIllegalCharacters;
}
bool GameProjectUtils::ProjectFileExists(const FString& ProjectFile)
{
return FPlatformFileManager::Get().GetPlatformFile().FileExists(*ProjectFile);
}
bool GameProjectUtils::AnyProjectFilesExistInFolder(const FString& Path)
{
TArray<FString> ExistingFiles;
const FString Wildcard = FString::Printf(TEXT("%s/*.%s"), *Path, *FProjectDescriptor::GetExtension());
IFileManager::Get().FindFiles(ExistingFiles, *Wildcard, /*Files=*/true, /*Directories=*/false);
return ExistingFiles.Num() > 0;
}
bool GameProjectUtils::CleanupIsEnabled()
{
// Clean up files when running Rocket (unless otherwise specified on the command line)
return FParse::Param(FCommandLine::Get(), TEXT("norocketcleanup")) == false;
}
void GameProjectUtils::DeleteCreatedFiles(const FString& RootFolder, const TArray<FString>& CreatedFiles)
{
if (CleanupIsEnabled())
{
for ( auto FileToDeleteIt = CreatedFiles.CreateConstIterator(); FileToDeleteIt; ++FileToDeleteIt )
{
IFileManager::Get().Delete(**FileToDeleteIt);
}
// If the project folder is empty after deleting all the files we created, delete the directory as well
TArray<FString> RemainingFiles;
IFileManager::Get().FindFilesRecursive(RemainingFiles, *RootFolder, TEXT("*.*"), /*Files=*/true, /*Directories=*/false);
if ( RemainingFiles.Num() == 0 )
{
IFileManager::Get().DeleteDirectory(*RootFolder, /*RequireExists=*/false, /*Tree=*/true);
}
}
}
FString GameProjectUtils::GetHardwareConfigString(const FProjectInformation& InProjectInfo)
{
FString HardwareTargeting;
FString TargetHardwareAsString;
UEnum::GetValueAsString(TEXT("/Script/HardwareTargeting.EHardwareClass"), InProjectInfo.TargetedHardware, /*out*/ TargetHardwareAsString);
FString GraphicsPresetAsString;
UEnum::GetValueAsString(TEXT("/Script/HardwareTargeting.EGraphicsPreset"), InProjectInfo.DefaultGraphicsPerformance, /*out*/ GraphicsPresetAsString);
HardwareTargeting += TEXT("[/Script/HardwareTargeting.HardwareTargetingSettings]") LINE_TERMINATOR;
HardwareTargeting += FString::Printf(TEXT("TargetedHardwareClass=%s") LINE_TERMINATOR, *TargetHardwareAsString);
HardwareTargeting += FString::Printf(TEXT("DefaultGraphicsPerformance=%s") LINE_TERMINATOR, *GraphicsPresetAsString);
HardwareTargeting += LINE_TERMINATOR;
return HardwareTargeting;
}
bool GameProjectUtils::GenerateConfigFiles(const FProjectInformation& InProjectInfo, TArray<FString>& OutCreatedFiles, FText& OutFailReason)
{
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
FString ProjectConfigPath = NewProjectFolder / TEXT("Config");
// DefaultEngine.ini
{
const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini");
FString FileContents;
FileContents += TEXT("[URL]") LINE_TERMINATOR;
FileContents += GetHardwareConfigString(InProjectInfo);
FileContents += LINE_TERMINATOR;
if (InProjectInfo.bCopyStarterContent)
{
FString SpecificEditorStartupMap;
FString SpecificGameDefaultMap;
// If we have starter content packs available, specify starter map
if( IsStarterContentAvailableForNewProjects() == true )
{
if (InProjectInfo.TargetedHardware == EHardwareClass::Mobile)
{
SpecificEditorStartupMap = TEXT("/Game/MobileStarterContent/Maps/Minimal_Default");
SpecificGameDefaultMap = TEXT("/Game/MobileStarterContent/Maps/Minimal_Default");
}
else
{
SpecificEditorStartupMap = TEXT("/Game/StarterContent/Maps/Minimal_Default");
SpecificGameDefaultMap = TEXT("/Game/StarterContent/Maps/Minimal_Default");
}
}
// Write out the settings for startup map and game default map
FileContents += TEXT("[/Script/EngineSettings.GameMapsSettings]") LINE_TERMINATOR;
FileContents += FString::Printf(TEXT("EditorStartupMap=%s") LINE_TERMINATOR, *SpecificEditorStartupMap);
FileContents += FString::Printf(TEXT("GameDefaultMap=%s") LINE_TERMINATOR, *SpecificGameDefaultMap);
if (InProjectInfo.bShouldGenerateCode)
{
FileContents += FString::Printf(TEXT("GlobalDefaultGameMode=\"/Script/%s.%sGameMode\"") LINE_TERMINATOR, *NewProjectName, *NewProjectName);
}
}
if (WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason))
{
OutCreatedFiles.Add(DefaultEngineIniFilename);
}
else
{
return false;
}
}
// DefaultEditor.ini
{
const FString DefaultEditorIniFilename = ProjectConfigPath / TEXT("DefaultEditor.ini");
FString FileContents;
if (WriteOutputFile(DefaultEditorIniFilename, FileContents, OutFailReason))
{
OutCreatedFiles.Add(DefaultEditorIniFilename);
}
else
{
return false;
}
}
// DefaultGame.ini
{
const FString DefaultGameIniFilename = ProjectConfigPath / TEXT("DefaultGame.ini");
FString FileContents;
FileContents += TEXT("[/Script/EngineSettings.GeneralProjectSettings]") LINE_TERMINATOR;
FileContents += TEXT("ProjectID=") + FGuid::NewGuid().ToString() + LINE_TERMINATOR;
if (WriteOutputFile(DefaultGameIniFilename, FileContents, OutFailReason))
{
OutCreatedFiles.Add(DefaultGameIniFilename);
}
else
{
return false;
}
}
return true;
}
bool GameProjectUtils::GenerateBasicSourceCode(TArray<FString>& OutCreatedFiles, FText& OutFailReason)
{
TArray<FString> StartupModuleNames;
if (GameProjectUtils::GenerateBasicSourceCode(FPaths::GameSourceDir().LeftChop(1), FApp::GetGameName(), FPaths::GameDir(), StartupModuleNames, OutCreatedFiles, OutFailReason))
{
GameProjectUtils::UpdateProject(
FProjectDescriptorModifier::CreateLambda(
[&StartupModuleNames](FProjectDescriptor& Descriptor)
{
return UpdateStartupModuleNames(Descriptor, &StartupModuleNames);
}));
return true;
}
return false;
}
bool GameProjectUtils::GenerateBasicSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, const FString& NewProjectRoot, TArray<FString>& OutGeneratedStartupModuleNames, TArray<FString>& OutCreatedFiles, FText& OutFailReason)
{
const FString GameModulePath = NewProjectSourcePath / NewProjectName;
const FString EditorName = NewProjectName + TEXT("Editor");
// MyGame.Build.cs
{
const FString NewBuildFilename = GameModulePath / NewProjectName + TEXT(".Build.cs");
TArray<FString> PublicDependencyModuleNames;
PublicDependencyModuleNames.Add(TEXT("Core"));
PublicDependencyModuleNames.Add(TEXT("CoreUObject"));
PublicDependencyModuleNames.Add(TEXT("Engine"));
PublicDependencyModuleNames.Add(TEXT("InputCore"));
TArray<FString> PrivateDependencyModuleNames;
if ( GenerateGameModuleBuildFile(NewBuildFilename, NewProjectName, PublicDependencyModuleNames, PrivateDependencyModuleNames, OutFailReason) )
{
OutGeneratedStartupModuleNames.Add(NewProjectName);
OutCreatedFiles.Add(NewBuildFilename);
}
else
{
return false;
}
}
// MyGame.Target.cs
{
const FString NewTargetFilename = NewProjectSourcePath / NewProjectName + TEXT(".Target.cs");
TArray<FString> ExtraModuleNames;
ExtraModuleNames.Add( NewProjectName );
if ( GenerateGameModuleTargetFile(NewTargetFilename, NewProjectName, ExtraModuleNames, OutFailReason) )
{
OutCreatedFiles.Add(NewTargetFilename);
}
else
{
return false;
}
}
// MyGameEditor.Target.cs
{
const FString NewTargetFilename = NewProjectSourcePath / EditorName + TEXT(".Target.cs");
// Include the MyGame module...
TArray<FString> ExtraModuleNames;
ExtraModuleNames.Add(NewProjectName);
if ( GenerateEditorModuleTargetFile(NewTargetFilename, EditorName, ExtraModuleNames, OutFailReason) )
{
OutCreatedFiles.Add(NewTargetFilename);
}
else
{
return false;
}
}
// MyGame.h
{
const FString NewHeaderFilename = GameModulePath / NewProjectName + TEXT(".h");
TArray<FString> PublicHeaderIncludes;
PublicHeaderIncludes.Add(TEXT("Engine.h"));
if ( GenerateGameModuleHeaderFile(NewHeaderFilename, PublicHeaderIncludes, OutFailReason) )
{
OutCreatedFiles.Add(NewHeaderFilename);
}
else
{
return false;
}
}
// MyGame.cpp
{
const FString NewCPPFilename = GameModulePath / NewProjectName + TEXT(".cpp");
if ( GenerateGameModuleCPPFile(NewCPPFilename, NewProjectName, NewProjectName, OutFailReason) )
{
OutCreatedFiles.Add(NewCPPFilename);
}
else
{
return false;
}
}
return true;
}
bool GameProjectUtils::GenerateGameFrameworkSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, TArray<FString>& OutCreatedFiles, FText& OutFailReason)
{
const FString GameModulePath = NewProjectSourcePath / NewProjectName;
// Used to override the code generation validation since the module we're creating isn't the same as the project we currently have loaded
FModuleContextInfo NewModuleInfo;
NewModuleInfo.ModuleName = NewProjectName;
NewModuleInfo.ModuleType = EHostType::Runtime;
NewModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(GameModulePath / ""); // Ensure trailing /
// MyGameGameMode.h
{
const UClass* BaseClass = AGameModeBase::StaticClass();
const FString NewClassName = NewProjectName + BaseClass->GetName();
const FString NewHeaderFilename = GameModulePath / NewClassName + TEXT(".h");
FString UnusedSyncLocation;
if ( GenerateClassHeaderFile(NewHeaderFilename, NewClassName, FNewClassInfo(BaseClass), TArray<FString>(), TEXT(""), TEXT(""), UnusedSyncLocation, NewModuleInfo, false, OutFailReason) )
{
OutCreatedFiles.Add(NewHeaderFilename);
}
else
{
return false;
}
}
// MyGameGameMode.cpp
{
const UClass* BaseClass = AGameModeBase::StaticClass();
const FString NewClassName = NewProjectName + BaseClass->GetName();
const FString NewCPPFilename = GameModulePath / NewClassName + TEXT(".cpp");
TArray<FString> PropertyOverrides;
TArray<FString> AdditionalIncludes;
FString UnusedSyncLocation;
if ( GenerateClassCPPFile(NewCPPFilename, NewClassName, FNewClassInfo(BaseClass), AdditionalIncludes, PropertyOverrides, TEXT(""), UnusedSyncLocation, NewModuleInfo, OutFailReason) )
{
OutCreatedFiles.Add(NewCPPFilename);
}
else
{
return false;
}
}
return true;
}
bool GameProjectUtils::BuildCodeProject(const FString& ProjectFilename)
{
// Build the project while capturing the log output. Passing GWarn to CompileGameProject will allow Slate to display the progress bar.
FStringOutputDevice OutputLog;
OutputLog.SetAutoEmitLineTerminator(true);
GLog->AddOutputDevice(&OutputLog);
bool bCompileSucceeded = FDesktopPlatformModule::Get()->CompileGameProject(FPaths::RootDir(), ProjectFilename, GWarn);
GLog->RemoveOutputDevice(&OutputLog);
// Try to compile the modules
if(!bCompileSucceeded)
{
FText DevEnvName = FSourceCodeNavigation::GetSuggestedSourceCodeIDE( true );
TArray<FText> CompileFailedButtons;
int32 OpenIDEButton = CompileFailedButtons.Add(FText::Format(LOCTEXT("CompileFailedOpenIDE", "Open with {0}"), DevEnvName));
CompileFailedButtons.Add(LOCTEXT("CompileFailedCancel", "Cancel"));
FText LogText = FText::FromString(OutputLog.Replace(LINE_TERMINATOR, TEXT("\n")).TrimTrailing());
int32 CompileFailedChoice = SOutputLogDialog::Open(LOCTEXT("CompileFailedTitle", "Compile Failed"), FText::Format(LOCTEXT("CompileFailedHeader", "The project could not be compiled. Would you like to open it in {0}?"), DevEnvName), LogText, FText::GetEmpty(), CompileFailedButtons);
FText FailReason;
if(CompileFailedChoice == OpenIDEButton && !GameProjectUtils::OpenCodeIDE(ProjectFilename, FailReason))
{
FMessageDialog::Open(EAppMsgType::Ok, FailReason);
}
}
return bCompileSucceeded;
}
bool GameProjectUtils::GenerateCodeProjectFiles(const FString& ProjectFilename, FText& OutFailReason, FText& OutFailLog)
{
FStringOutputDevice OutputLog;
OutputLog.SetAutoEmitLineTerminator(true);
GLog->AddOutputDevice(&OutputLog);
bool bHaveProjectFiles = FDesktopPlatformModule::Get()->GenerateProjectFiles(FPaths::RootDir(), ProjectFilename, GWarn);
GLog->RemoveOutputDevice(&OutputLog);
if ( !bHaveProjectFiles )
{
OutFailReason = LOCTEXT("ErrorWhileGeneratingProjectFiles", "An error occurred while trying to generate project files.");
OutFailLog = FText::FromString(OutputLog);
return false;
}
return true;
}
bool GameProjectUtils::IsStarterContentAvailableForNewProjects()
{
TArray<FString> StarterContentFiles;
GetStarterContentFiles(StarterContentFiles);
bool bHasStaterContent = StarterContentFiles.FindByPredicate([&](const FString& Str){ return Str.Contains("StarterContent"); }) != nullptr;
return bHasStaterContent;
}
TArray<FModuleContextInfo> GameProjectUtils::GetCurrentProjectModules()
{
const FProjectDescriptor* const CurrentProject = IProjectManager::Get().GetCurrentProject();
check(CurrentProject);
TArray<FModuleContextInfo> RetModuleInfos;
if (!GameProjectUtils::ProjectHasCodeFiles() || CurrentProject->Modules.Num() == 0)
{
// If this project doesn't currently have any code in it, we need to add a dummy entry for the game
// so that we can still use the class wizard (this module will be created once we add a class)
FModuleContextInfo ModuleInfo;
ModuleInfo.ModuleName = FApp::GetGameName();
ModuleInfo.ModuleType = EHostType::Runtime;
ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(FPaths::GameSourceDir() / ModuleInfo.ModuleName / ""); // Ensure trailing /
RetModuleInfos.Emplace(ModuleInfo);
}
// Resolve out the paths for each module and add the cut-down into to our output array
for (const FModuleDescriptor& ModuleDesc : CurrentProject->Modules)
{
FModuleContextInfo ModuleInfo;
ModuleInfo.ModuleName = ModuleDesc.Name.ToString();
ModuleInfo.ModuleType = ModuleDesc.Type;
// Try and find the .Build.cs file for this module within our currently loaded project's Source directory
FString TmpPath;
if (!FindSourceFileInProject(ModuleInfo.ModuleName + ".Build.cs", FPaths::GameSourceDir(), TmpPath))
{
continue;
}
// Chop the .Build.cs file off the end of the path
ModuleInfo.ModuleSourcePath = FPaths::GetPath(TmpPath);
ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(ModuleInfo.ModuleSourcePath / ""); // Ensure trailing /
RetModuleInfos.Emplace(ModuleInfo);
}
return RetModuleInfos;
}
TArray<FModuleContextInfo> GameProjectUtils::GetCurrentProjectPluginModules()
{
const FProjectDescriptor* const CurrentProject = IProjectManager::Get().GetCurrentProject();
check(CurrentProject);
TArray<FModuleContextInfo> RetModuleInfos;
if (!GameProjectUtils::ProjectHasCodeFiles() || CurrentProject->Modules.Num() == 0)
{
// Don't get plugins if the game project has no source tree.
return RetModuleInfos;
}
// Resolve out the paths for each module and add the cut-down into to our output array
for (const auto& Plugin : IPluginManager::Get().GetDiscoveredPlugins())
{
// Only get plugins that are a part of the game project
if (Plugin->GetLoadedFrom() == EPluginLoadedFrom::GameProject)
{
for (const auto& PluginModule : Plugin->GetDescriptor().Modules)
{
FModuleContextInfo ModuleInfo;
ModuleInfo.ModuleName = PluginModule.Name.ToString();
ModuleInfo.ModuleType = PluginModule.Type;
// Try and find the .Build.cs file for this module within the plugin source tree
FString TmpPath;
if (!FindSourceFileInProject(ModuleInfo.ModuleName + ".Build.cs", Plugin->GetBaseDir(), TmpPath))
{
continue;
}
// Chop the .Build.cs file off the end of the path
ModuleInfo.ModuleSourcePath = FPaths::GetPath(TmpPath);
ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(ModuleInfo.ModuleSourcePath / ""); // Ensure trailing /
RetModuleInfos.Emplace(ModuleInfo);
}
}
}
return RetModuleInfos;
}
bool GameProjectUtils::IsValidSourcePath(const FString& InPath, const FModuleContextInfo& ModuleInfo, FText* const OutFailReason)
{
const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing /
// Validate the path contains no invalid characters
if(!FPaths::ValidatePath(AbsoluteInPath, OutFailReason))
{
return false;
}
if(!AbsoluteInPath.StartsWith(ModuleInfo.ModuleSourcePath))
{
if(OutFailReason)
{
FFormatNamedArguments Args;
Args.Add(TEXT("ModuleName"), FText::FromString(ModuleInfo.ModuleName));
Args.Add(TEXT("RootSourcePath"), FText::FromString(ModuleInfo.ModuleSourcePath));
*OutFailReason = FText::Format( LOCTEXT("SourcePathInvalidForModule", "All source code for '{ModuleName}' must exist within '{RootSourcePath}'"), Args );
}
return false;
}
return true;
}
bool GameProjectUtils::CalculateSourcePaths(const FString& InPath, const FModuleContextInfo& ModuleInfo, FString& OutHeaderPath, FString& OutSourcePath, FText* const OutFailReason)
{
const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing /
OutHeaderPath = AbsoluteInPath;
OutSourcePath = AbsoluteInPath;
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
if(!GetClassLocation(InPath, ModuleInfo, ClassPathLocation, OutFailReason))
{
return false;
}
const FString RootPath = ModuleInfo.ModuleSourcePath;
const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing /
const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing /
const FString ClassesPath = RootPath / "Classes" / ""; // Ensure trailing /
// The root path must exist; we will allow the creation of sub-folders, but not the module root!
// We ignore this check if the project doesn't already have source code in it, as the module folder won't yet have been created
const bool bHasCodeFiles = GameProjectUtils::ProjectHasCodeFiles();
if(!IFileManager::Get().DirectoryExists(*RootPath) && bHasCodeFiles)
{
if(OutFailReason)
{
FFormatNamedArguments Args;
Args.Add(TEXT("ModuleSourcePath"), FText::FromString(RootPath));
*OutFailReason = FText::Format(LOCTEXT("SourcePathMissingModuleRoot", "The specified module path does not exist on disk: {ModuleSourcePath}"), Args);
}
return false;
}
// The rules for placing header files are as follows:
// 1) If InPath is the source root, and GetClassLocation has said the class header should be in the Public folder, put it in the Public folder
// 2) Otherwise, just place the header at InPath (the default set above)
if(AbsoluteInPath == RootPath)
{
OutHeaderPath = (ClassPathLocation == EClassLocation::Public) ? PublicPath : AbsoluteInPath;
}
// The rules for placing source files are as follows:
// 1) If InPath is the source root, and GetClassLocation has said the class header should be in the Public folder, put the source file in the Private folder
// 2) If InPath is contained within the Public or Classes folder of this module, place it in the equivalent path in the Private folder
// 3) Otherwise, just place the source file at InPath (the default set above)
if(AbsoluteInPath == RootPath)
{
OutSourcePath = (ClassPathLocation == EClassLocation::Public) ? PrivatePath : AbsoluteInPath;
}
else if(ClassPathLocation == EClassLocation::Public)
{
OutSourcePath = AbsoluteInPath.Replace(*PublicPath, *PrivatePath);
}
else if(ClassPathLocation == EClassLocation::Classes)
{
OutSourcePath = AbsoluteInPath.Replace(*ClassesPath, *PrivatePath);
}
return !OutHeaderPath.IsEmpty() && !OutSourcePath.IsEmpty();
}
bool GameProjectUtils::GetClassLocation(const FString& InPath, const FModuleContextInfo& ModuleInfo, EClassLocation& OutClassLocation, FText* const OutFailReason)
{
const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing /
OutClassLocation = EClassLocation::UserDefined;
if(!IsValidSourcePath(InPath, ModuleInfo, OutFailReason))
{
return false;
}
const FString RootPath = ModuleInfo.ModuleSourcePath;
const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing /
const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing /
const FString ClassesPath = RootPath / "Classes" / ""; // Ensure trailing /
// If either the Public or Private path exists, and we're in the root, force the header/source file to use one of these folders
const bool bPublicPathExists = IFileManager::Get().DirectoryExists(*PublicPath);
const bool bPrivatePathExists = IFileManager::Get().DirectoryExists(*PrivatePath);
const bool bForceInternalPath = AbsoluteInPath == RootPath && (bPublicPathExists || bPrivatePathExists);
if(AbsoluteInPath == RootPath)
{
OutClassLocation = (bPublicPathExists || bForceInternalPath) ? EClassLocation::Public : EClassLocation::UserDefined;
}
else if(AbsoluteInPath.StartsWith(PublicPath))
{
OutClassLocation = EClassLocation::Public;
}
else if(AbsoluteInPath.StartsWith(PrivatePath))
{
OutClassLocation = EClassLocation::Private;
}
else if(AbsoluteInPath.StartsWith(ClassesPath))
{
OutClassLocation = EClassLocation::Classes;
}
else
{
OutClassLocation = EClassLocation::UserDefined;
}
return true;
}
GameProjectUtils::EProjectDuplicateResult GameProjectUtils::DuplicateProjectForUpgrade( const FString& InProjectFile, FString& OutNewProjectFile )
{
IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
// Get the directory part of the project name
FString OldDirectoryName = FPaths::GetPath(InProjectFile);
FPaths::NormalizeDirectoryName(OldDirectoryName);
FString NewDirectoryName = OldDirectoryName;
// Strip off any previous version number from the project name
for(int32 LastSpace; NewDirectoryName.FindLastChar(' ', LastSpace); )
{
const TCHAR *End = *NewDirectoryName + LastSpace + 1;
if(End[0] != '4' || End[1] != '.' || !FChar::IsDigit(End[2]))
{
break;
}
End += 3;
while(FChar::IsDigit(*End))
{
End++;
}
if(*End != 0)
{
break;
}
NewDirectoryName = NewDirectoryName.Left(LastSpace).TrimTrailing();
}
// Append the new version number
NewDirectoryName += FString::Printf(TEXT(" %s"), *FEngineVersion::Current().ToString(EVersionComponent::Minor));
// Find a directory name that doesn't exist
FString BaseDirectoryName = NewDirectoryName;
for(int32 Idx = 2; IFileManager::Get().DirectoryExists(*NewDirectoryName); Idx++)
{
NewDirectoryName = FString::Printf(TEXT("%s - %d"), *BaseDirectoryName, Idx);
}
// Recursively find all the files we need to copy, excluding those that are within the directories listed in SourceDirectoriesToSkip
struct FGatherFilesToCopyHelper
{
public:
FGatherFilesToCopyHelper(FString InRootSourceDirectory)
: RootSourceDirectory(MoveTemp(InRootSourceDirectory))
{
static const FString RelativeDirectoriesToSkip[] = {
TEXT("Binaries"),
TEXT("DerivedDataCache"),
TEXT("Intermediate"),
TEXT("Saved/Autosaves"),
TEXT("Saved/Backup"),
TEXT("Saved/Config"),
TEXT("Saved/Cooked"),
TEXT("Saved/HardwareSurvey"),
TEXT("Saved/Logs"),
TEXT("Saved/StagedBuilds"),
};
SourceDirectoriesToSkip.Reserve(ARRAY_COUNT(RelativeDirectoriesToSkip));
for (const FString& RelativeDirectoryToSkip : RelativeDirectoriesToSkip)
{
SourceDirectoriesToSkip.Emplace(RootSourceDirectory / RelativeDirectoryToSkip);
}
}
void GatherFilesToCopy(TArray<FString>& OutSourceDirectories, TArray<FString>& OutSourceFiles)
{
GatherFilesToCopy(RootSourceDirectory, OutSourceDirectories, OutSourceFiles);
}
private:
void GatherFilesToCopy(const FString& InSourceDirectoryPath, TArray<FString>& OutSourceDirectories, TArray<FString>& OutSourceFiles)
{
const FString SourceDirectorySearchWildcard = InSourceDirectoryPath / TEXT("*");
OutSourceDirectories.Emplace(InSourceDirectoryPath);
TArray<FString> SourceFilenames;
IFileManager::Get().FindFiles(SourceFilenames, *SourceDirectorySearchWildcard, true, false);
OutSourceFiles.Reserve(OutSourceFiles.Num() + SourceFilenames.Num());
for (const FString& SourceFilename : SourceFilenames)
{
OutSourceFiles.Emplace(InSourceDirectoryPath / SourceFilename);
}
TArray<FString> SourceSubDirectoryNames;
IFileManager::Get().FindFiles(SourceSubDirectoryNames, *SourceDirectorySearchWildcard, false, true);
for (const FString& SourceSubDirectoryName : SourceSubDirectoryNames)
{
const FString SourceSubDirectoryPath = InSourceDirectoryPath / SourceSubDirectoryName;
if (!SourceDirectoriesToSkip.Contains(SourceSubDirectoryPath))
{
GatherFilesToCopy(SourceSubDirectoryPath, OutSourceDirectories, OutSourceFiles);
}
}
}
FString RootSourceDirectory;
TArray<FString> SourceDirectoriesToSkip;
};
TArray<FString> SourceDirectories;
TArray<FString> SourceFiles;
FGatherFilesToCopyHelper(OldDirectoryName).GatherFilesToCopy(SourceDirectories, SourceFiles);
// Copy everything
bool bCopySucceeded = true;
bool bUserCanceled = false;
GWarn->BeginSlowTask(LOCTEXT("CreatingCopyOfProject", "Creating copy of project..."), true, true);
for(int32 Idx = 0; Idx < SourceDirectories.Num() && bCopySucceeded; Idx++)
{
FString TargetDirectory = NewDirectoryName + SourceDirectories[Idx].Mid(OldDirectoryName.Len());
bUserCanceled = GWarn->ReceivedUserCancel();
bCopySucceeded = !bUserCanceled && PlatformFile.CreateDirectory(*TargetDirectory);
GWarn->UpdateProgress(Idx + 1, SourceDirectories.Num() + SourceFiles.Num());
}
for(int32 Idx = 0; Idx < SourceFiles.Num() && bCopySucceeded; Idx++)
{
FString TargetFile = NewDirectoryName + SourceFiles[Idx].Mid(OldDirectoryName.Len());
bUserCanceled = GWarn->ReceivedUserCancel();
bCopySucceeded = !bUserCanceled && PlatformFile.CopyFile(*TargetFile, *SourceFiles[Idx]);
GWarn->UpdateProgress(SourceDirectories.Num() + Idx + 1, SourceDirectories.Num() + SourceFiles.Num());
}
GWarn->EndSlowTask();
// Wipe the directory if the user canceled or we couldn't update
if(!bCopySucceeded)
{
PlatformFile.DeleteDirectoryRecursively(*NewDirectoryName);
if(bUserCanceled)
{
return EProjectDuplicateResult::UserCanceled;
}
else
{
return EProjectDuplicateResult::Failed;
}
}
// Otherwise fixup the output project filename
OutNewProjectFile = NewDirectoryName / FPaths::GetCleanFilename(InProjectFile);
return EProjectDuplicateResult::Succeeded;
}
void GameProjectUtils::UpdateSupportedTargetPlatforms(const FName& InPlatformName, const bool bIsSupported)
{
const FString& ProjectFilename = FPaths::GetProjectFilePath();
if(!ProjectFilename.IsEmpty())
{
// First attempt to check out the file if SCC is enabled
if(ISourceControlModule::Get().IsEnabled())
{
FText UnusedFailReason;
CheckoutGameProjectFile(ProjectFilename, UnusedFailReason);
}
// Second make sure the file is writable
if(FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename))
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false);
}
IProjectManager::Get().UpdateSupportedTargetPlatformsForCurrentProject(InPlatformName, bIsSupported);
}
}
void GameProjectUtils::ClearSupportedTargetPlatforms()
{
const FString& ProjectFilename = FPaths::GetProjectFilePath();
if(!ProjectFilename.IsEmpty())
{
// First attempt to check out the file if SCC is enabled
if(ISourceControlModule::Get().IsEnabled())
{
FText UnusedFailReason;
CheckoutGameProjectFile(ProjectFilename, UnusedFailReason);
}
// Second make sure the file is writable
if(FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename))
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false);
}
IProjectManager::Get().ClearSupportedTargetPlatformsForCurrentProject();
}
}
void GameProjectUtils::UpdateAdditionalPluginDirectory(const FString& InDir, const bool bAddOrRemove)
{
const FString& ProjectFilename = FPaths::GetProjectFilePath();
if (!ProjectFilename.IsEmpty())
{
// First attempt to check out the file if SCC is enabled
if (ISourceControlModule::Get().IsEnabled())
{
FText UnusedFailReason;
CheckoutGameProjectFile(ProjectFilename, UnusedFailReason);
}
// Second make sure the file is writable
if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename))
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false);
}
IProjectManager::Get().UpdateAdditionalPluginDirectory(InDir, bAddOrRemove);
}
}
bool GameProjectUtils::ReadTemplateFile(const FString& TemplateFileName, FString& OutFileContents, FText& OutFailReason)
{
const FString FullFileName = FPaths::EngineContentDir() / TEXT("Editor") / TEXT("Templates") / TemplateFileName;
if ( FFileHelper::LoadFileToString(OutFileContents, *FullFileName) )
{
return true;
}
FFormatNamedArguments Args;
Args.Add( TEXT("FullFileName"), FText::FromString( FullFileName ) );
OutFailReason = FText::Format( LOCTEXT("FailedToReadTemplateFile", "Failed to read template file \"{FullFileName}\""), Args );
return false;
}
bool GameProjectUtils::WriteOutputFile(const FString& OutputFilename, const FString& OutputFileContents, FText& OutFailReason)
{
if ( FFileHelper::SaveStringToFile(OutputFileContents, *OutputFilename ) )
{
return true;
}
FFormatNamedArguments Args;
Args.Add( TEXT("OutputFilename"), FText::FromString( OutputFilename ) );
OutFailReason = FText::Format( LOCTEXT("FailedToWriteOutputFile", "Failed to write output file \"{OutputFilename}\". Perhaps the file is Read-Only?"), Args );
return false;
}
FString GameProjectUtils::MakeCopyrightLine()
{
const FString CopyrightNotice = GetDefault<UGeneralProjectSettings>()->CopyrightNotice;
if (!CopyrightNotice.IsEmpty())
{
return FString(TEXT("// ")) + CopyrightNotice;
}
else
{
return FString();
}
}
FString GameProjectUtils::MakeCommaDelimitedList(const TArray<FString>& InList, bool bPlaceQuotesAroundEveryElement)
{
FString ReturnString;
for ( auto ListIt = InList.CreateConstIterator(); ListIt; ++ListIt )
{
FString ElementStr;
if ( bPlaceQuotesAroundEveryElement )
{
ElementStr = FString::Printf( TEXT("\"%s\""), **ListIt);
}
else
{
ElementStr = *ListIt;
}
if ( ReturnString.Len() > 0 )
{
// If this is not the first item in the list, prepend with a comma
ElementStr = FString::Printf(TEXT(", %s"), *ElementStr);
}
ReturnString += ElementStr;
}
return ReturnString;
}
FString GameProjectUtils::MakeIncludeList(const TArray<FString>& InList)
{
FString ReturnString;
for ( auto ListIt = InList.CreateConstIterator(); ListIt; ++ListIt )
{
ReturnString += FString::Printf( TEXT("#include \"%s\"") LINE_TERMINATOR, **ListIt);
}
return ReturnString;
}
FString GameProjectUtils::DetermineModuleIncludePath(const FModuleContextInfo& ModuleInfo, const FString& FileRelativeTo)
{
FString ModuleIncludePath;
if(FindSourceFileInProject(ModuleInfo.ModuleName + ".h", ModuleInfo.ModuleSourcePath, ModuleIncludePath))
{
// Work out where the module header is;
// if it's Public then we can include it without any path since all Public and Classes folders are on the include path
// if it's located elsewhere, then we'll need to include it relative to the module source root as we can't guarantee
// that other folders are on the include paths
EClassLocation ModuleLocation;
if(GetClassLocation(ModuleIncludePath, ModuleInfo, ModuleLocation))
{
if(ModuleLocation == EClassLocation::Public || ModuleLocation == EClassLocation::Classes)
{
ModuleIncludePath = ModuleInfo.ModuleName + ".h";
}
else
{
// If the path to our new class is the same as the path to the module, we can include it directly
const FString ModulePath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(ModuleIncludePath));
const FString ClassPath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(FileRelativeTo));
if(ModulePath == ClassPath)
{
ModuleIncludePath = ModuleInfo.ModuleName + ".h";
}
else
{
// Updates ModuleIncludePath internally
if(!FPaths::MakePathRelativeTo(ModuleIncludePath, *ModuleInfo.ModuleSourcePath))
{
// Failed; just assume we can include it without any relative path
ModuleIncludePath = ModuleInfo.ModuleName + ".h";
}
}
}
}
else
{
// Failed; just assume we can include it without any relative path
ModuleIncludePath = ModuleInfo.ModuleName + ".h";
}
}
else
{
// This could potentially fail when generating new projects if the module file hasn't yet been created; just assume we can include it without any relative path
ModuleIncludePath = ModuleInfo.ModuleName + ".h";
}
return ModuleIncludePath;
}
/**
* Generates UObject class constructor definition with property overrides.
*
* @param Out String to assign generated constructor to.
* @param PrefixedClassName Prefixed class name for which we generate the constructor.
* @param PropertyOverridesStr String with property overrides in the constructor.
* @param OutFailReason Template read function failure reason.
*
* @returns True on success. False otherwise.
*/
bool GenerateConstructorDefinition(FString& Out, const FString& PrefixedClassName, const FString& PropertyOverridesStr, FText& OutFailReason)
{
FString Template;
if (!GameProjectUtils::ReadTemplateFile(TEXT("UObjectClassConstructorDefinition.template"), Template, OutFailReason))
{
return false;
}
Out = Template.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive);
Out = Out.Replace(TEXT("%PROPERTY_OVERRIDES%"), *PropertyOverridesStr, ESearchCase::CaseSensitive);
return true;
}
/**
* Generates UObject class constructor declaration.
*
* @param Out String to assign generated constructor to.
* @param PrefixedClassName Prefixed class name for which we generate the constructor.
* @param OutFailReason Template read function failure reason.
*
* @returns True on success. False otherwise.
*/
bool GenerateConstructorDeclaration(FString& Out, const FString& PrefixedClassName, FText& OutFailReason)
{
FString Template;
if (!GameProjectUtils::ReadTemplateFile(TEXT("UObjectClassConstructorDeclaration.template"), Template, OutFailReason))
{
return false;
}
Out = Template.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive);
return true;
}
bool GameProjectUtils::GenerateClassHeaderFile(const FString& NewHeaderFileName, const FString UnPrefixedClassName, const FNewClassInfo ParentClassInfo, const TArray<FString>& ClassSpecifierList, const FString& ClassProperties, const FString& ClassFunctionDeclarations, FString& OutSyncLocation, const FModuleContextInfo& ModuleInfo, bool bDeclareConstructor, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(ParentClassInfo.GetHeaderTemplateFilename(), Template, OutFailReason) )
{
return false;
}
const FString ClassPrefix = ParentClassInfo.GetClassPrefixCPP();
const FString PrefixedClassName = ClassPrefix + UnPrefixedClassName;
const FString PrefixedBaseClassName = ClassPrefix + ParentClassInfo.GetClassNameCPP();
FString BaseClassIncludeDirective;
FString BaseClassIncludePath;
if(ParentClassInfo.GetIncludePath(BaseClassIncludePath))
{
BaseClassIncludeDirective = FString::Printf(LINE_TERMINATOR TEXT("#include \"%s\""), *BaseClassIncludePath);
}
FString ModuleAPIMacro;
{
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
if ( GetClassLocation(NewHeaderFileName, ModuleInfo, ClassPathLocation) )
{
// If this class isn't Private, make sure and include the API macro so it can be linked within other modules
if ( ClassPathLocation != EClassLocation::Private )
{
ModuleAPIMacro = ModuleInfo.ModuleName.ToUpper() + "_API "; // include a trailing space for the template formatting
}
}
}
FString EventualConstructorDeclaration;
if (bDeclareConstructor)
{
if (!GenerateConstructorDeclaration(EventualConstructorDeclaration, PrefixedClassName, OutFailReason))
{
return false;
}
}
// Not all of these will exist in every class template
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%UNPREFIXED_CLASS_NAME%"), *UnPrefixedClassName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%CLASS_MODULE_API_MACRO%"), *ModuleAPIMacro, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%UCLASS_SPECIFIER_LIST%"), *MakeCommaDelimitedList(ClassSpecifierList, false), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_BASE_CLASS_NAME%"), *PrefixedBaseClassName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%EVENTUAL_CONSTRUCTOR_DECLARATION%"), *EventualConstructorDeclaration, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%CLASS_PROPERTIES%"), *ClassProperties, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%CLASS_FUNCTION_DECLARATIONS%"), *ClassFunctionDeclarations, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%BASE_CLASS_INCLUDE_DIRECTIVE%"), *BaseClassIncludeDirective, ESearchCase::CaseSensitive);
HarvestCursorSyncLocation( FinalOutput, OutSyncLocation );
return WriteOutputFile(NewHeaderFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateClassCPPFile(const FString& NewCPPFileName, const FString UnPrefixedClassName, const FNewClassInfo ParentClassInfo, const TArray<FString>& AdditionalIncludes, const TArray<FString>& PropertyOverrides, const FString& AdditionalMemberDefinitions, FString& OutSyncLocation, const FModuleContextInfo& ModuleInfo, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(ParentClassInfo.GetSourceTemplateFilename(), Template, OutFailReason) )
{
return false;
}
const FString ClassPrefix = ParentClassInfo.GetClassPrefixCPP();
const FString PrefixedClassName = ClassPrefix + UnPrefixedClassName;
const FString PrefixedBaseClassName = ClassPrefix + ParentClassInfo.GetClassNameCPP();
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
if ( !GetClassLocation(NewCPPFileName, ModuleInfo, ClassPathLocation, &OutFailReason) )
{
return false;
}
FString AdditionalIncludesStr;
for (int32 IncludeIdx = 0; IncludeIdx < AdditionalIncludes.Num(); ++IncludeIdx)
{
if (IncludeIdx > 0)
{
AdditionalIncludesStr += LINE_TERMINATOR;
}
AdditionalIncludesStr += FString::Printf(TEXT("#include \"%s\""), *AdditionalIncludes[IncludeIdx]);
}
FString PropertyOverridesStr;
for ( int32 OverrideIdx = 0; OverrideIdx < PropertyOverrides.Num(); ++OverrideIdx )
{
if ( OverrideIdx > 0 )
{
PropertyOverridesStr += LINE_TERMINATOR;
}
PropertyOverridesStr += TEXT("\t");
PropertyOverridesStr += *PropertyOverrides[OverrideIdx];
}
// Calculate the correct include path for the module header
const FString ModuleIncludePath = DetermineModuleIncludePath(ModuleInfo, NewCPPFileName);
FString EventualConstructorDefinition;
if (PropertyOverrides.Num() != 0)
{
if (!GenerateConstructorDefinition(EventualConstructorDefinition, PrefixedClassName, PropertyOverridesStr, OutFailReason))
{
return false;
}
}
// Not all of these will exist in every class template
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%UNPREFIXED_CLASS_NAME%"), *UnPrefixedClassName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleInfo.ModuleName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_INCLUDE_PATH%"), *ModuleIncludePath, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%EVENTUAL_CONSTRUCTOR_DEFINITION%"), *EventualConstructorDefinition, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%ADDITIONAL_MEMBER_DEFINITIONS%"), *AdditionalMemberDefinitions, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%ADDITIONAL_INCLUDE_DIRECTIVES%"), *AdditionalIncludesStr, ESearchCase::CaseSensitive);
HarvestCursorSyncLocation( FinalOutput, OutSyncLocation );
return WriteOutputFile(NewCPPFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateGameModuleBuildFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray<FString>& PublicDependencyModuleNames, const TArray<FString>& PrivateDependencyModuleNames, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("GameModule.Build.cs.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PublicDependencyModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PRIVATE_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PrivateDependencyModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateGameModuleTargetFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray<FString>& ExtraModuleNames, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("Stub.Target.cs.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%EXTRA_MODULE_NAMES%"), *MakeCommaDelimitedList(ExtraModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%TARGET_TYPE%"), TEXT("Game"), ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateEditorModuleBuildFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray<FString>& PublicDependencyModuleNames, const TArray<FString>& PrivateDependencyModuleNames, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("EditorModule.Build.cs.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PublicDependencyModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PRIVATE_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PrivateDependencyModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateEditorModuleTargetFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray<FString>& ExtraModuleNames, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("Stub.Target.cs.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%EXTRA_MODULE_NAMES%"), *MakeCommaDelimitedList(ExtraModuleNames), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%TARGET_TYPE%"), TEXT("Editor"), ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateGameModuleCPPFile(const FString& NewBuildFileName, const FString& ModuleName, const FString& GameName, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("GameModule.cpp.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%GAME_NAME%"), *GameName, ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GenerateGameModuleHeaderFile(const FString& NewBuildFileName, const TArray<FString>& PublicHeaderIncludes, FText& OutFailReason)
{
FString Template;
if ( !ReadTemplateFile(TEXT("GameModule.h.template"), Template, OutFailReason) )
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_HEADER_INCLUDES%"), *MakeIncludeList(PublicHeaderIncludes), ESearchCase::CaseSensitive);
return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GeneratePluginModuleCPPFile(const FString& CPPFileName, const FString& ModuleName, const FString& StartupSourceCode, FText& OutFailReason)
{
FString Template;
if (!ReadTemplateFile(TEXT("PluginModule.cpp.template"), Template, OutFailReason))
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%MODULE_STARTUP_CODE%"), *StartupSourceCode, ESearchCase::CaseSensitive);
return WriteOutputFile(CPPFileName, FinalOutput, OutFailReason);
}
bool GameProjectUtils::GeneratePluginModuleHeaderFile(const FString& HeaderFileName, const TArray<FString>& PublicHeaderIncludes, FText& OutFailReason)
{
FString Template;
if (!ReadTemplateFile(TEXT("PluginModule.h.template"), Template, OutFailReason))
{
return false;
}
FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive);
FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_HEADER_INCLUDES%"), *MakeIncludeList(PublicHeaderIncludes), ESearchCase::CaseSensitive);
return WriteOutputFile(HeaderFileName, FinalOutput, OutFailReason);
}
void GameProjectUtils::OnUpdateProjectConfirm()
{
UpdateProject();
}
void GameProjectUtils::UpdateProject(const FProjectDescriptorModifier& Modifier)
{
UpdateProject_Impl(&Modifier);
}
void GameProjectUtils::UpdateProject()
{
UpdateProject_Impl(nullptr);
}
void GameProjectUtils::UpdateProject_Impl(const FProjectDescriptorModifier* Modifier)
{
const FString& ProjectFilename = FPaths::GetProjectFilePath();
const FString& ShortFilename = FPaths::GetCleanFilename(ProjectFilename);
FText FailReason;
FText UpdateMessage;
SNotificationItem::ECompletionState NewCompletionState;
if (UpdateGameProjectFile_Impl(ProjectFilename, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), Modifier, FailReason))
{
// The project was updated successfully.
FFormatNamedArguments Args;
Args.Add( TEXT("ShortFilename"), FText::FromString( ShortFilename ) );
UpdateMessage = FText::Format( LOCTEXT("ProjectFileUpdateComplete", "{ShortFilename} was successfully updated."), Args );
NewCompletionState = SNotificationItem::CS_Success;
}
else
{
// The user chose to update, but the update failed. Notify the user.
FFormatNamedArguments Args;
Args.Add( TEXT("ShortFilename"), FText::FromString( ShortFilename ) );
Args.Add( TEXT("FailReason"), FailReason );
UpdateMessage = FText::Format( LOCTEXT("ProjectFileUpdateFailed", "{ShortFilename} failed to update. {FailReason}"), Args );
NewCompletionState = SNotificationItem::CS_Fail;
}
if ( UpdateGameProjectNotification.IsValid() )
{
UpdateGameProjectNotification.Pin()->SetCompletionState(NewCompletionState);
UpdateGameProjectNotification.Pin()->SetText(UpdateMessage);
UpdateGameProjectNotification.Pin()->ExpireAndFadeout();
UpdateGameProjectNotification.Reset();
}
}
void GameProjectUtils::UpdateProject(const TArray<FString>* StartupModuleNames)
{
UpdateProject(
FProjectDescriptorModifier::CreateLambda(
[StartupModuleNames](FProjectDescriptor& Desc)
{
if (StartupModuleNames != nullptr)
{
return UpdateStartupModuleNames(Desc, StartupModuleNames);
}
return false;
}));
}
void GameProjectUtils::OnUpdateProjectCancel()
{
if ( UpdateGameProjectNotification.IsValid() )
{
UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_None);
UpdateGameProjectNotification.Pin()->ExpireAndFadeout();
UpdateGameProjectNotification.Reset();
}
}
void GameProjectUtils::TryMakeProjectFileWriteable(const FString& ProjectFile)
{
// First attempt to check out the file if SCC is enabled
if ( ISourceControlModule::Get().IsEnabled() )
{
FText FailReason;
GameProjectUtils::CheckoutGameProjectFile(ProjectFile, FailReason);
}
// Check if it's writable
if(FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFile))
{
FText ShouldMakeProjectWriteable = LOCTEXT("ShouldMakeProjectWriteable_Message", "'{ProjectFilename}' is read-only and cannot be updated. Would you like to make it writeable?");
FFormatNamedArguments Arguments;
Arguments.Add( TEXT("ProjectFilename"), FText::FromString(ProjectFile));
if(FMessageDialog::Open(EAppMsgType::YesNo, FText::Format(ShouldMakeProjectWriteable, Arguments)) == EAppReturnType::Yes)
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFile, false);
}
}
}
bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFile, const FString& EngineIdentifier, const FProjectDescriptorModifier& Modifier, FText& OutFailReason)
{
return UpdateGameProjectFile_Impl(ProjectFile, EngineIdentifier, &Modifier, OutFailReason);
}
bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason)
{
return UpdateGameProjectFile_Impl(ProjectFile, EngineIdentifier, nullptr, OutFailReason);
}
bool GameProjectUtils::UpdateGameProjectFile_Impl(const FString& ProjectFile, const FString& EngineIdentifier, const FProjectDescriptorModifier* Modifier, FText& OutFailReason)
{
// Make sure we can write to the project file
TryMakeProjectFileWriteable(ProjectFile);
// Load the descriptor
FProjectDescriptor Descriptor;
if(Descriptor.Load(ProjectFile, OutFailReason))
{
if (Modifier && Modifier->IsBound() && !Modifier->Execute(Descriptor))
{
// If modifier returns false it means that we want to drop changes.
return true;
}
// Update file on disk
return Descriptor.Save(ProjectFile, OutFailReason) && FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFile, EngineIdentifier);
}
return false;
}
bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFilename, const FString& EngineIdentifier, const TArray<FString>* StartupModuleNames, FText& OutFailReason)
{
return UpdateGameProjectFile(ProjectFilename, EngineIdentifier,
FProjectDescriptorModifier::CreateLambda(
[StartupModuleNames](FProjectDescriptor& Desc)
{
if (StartupModuleNames != nullptr)
{
return UpdateStartupModuleNames(Desc, StartupModuleNames);
}
return false;
}
), OutFailReason);
}
bool GameProjectUtils::CheckoutGameProjectFile(const FString& ProjectFilename, FText& OutFailReason)
{
if ( !ensure(ProjectFilename.Len()) )
{
OutFailReason = LOCTEXT("NoProjectFilename", "The project filename was not specified.");
return false;
}
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutFailReason = LOCTEXT("SCCDisabled", "Source control is not enabled. Enable source control in the preferences menu.");
return false;
}
FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(ProjectFilename);
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
TArray<FString> FilesToBeCheckedOut;
FilesToBeCheckedOut.Add(AbsoluteFilename);
bool bSuccessfullyCheckedOut = false;
OutFailReason = LOCTEXT("SCCStateInvalid", "Could not determine source control state.");
if(SourceControlState.IsValid())
{
if(SourceControlState->IsCheckedOut() || SourceControlState->IsAdded() || !SourceControlState->IsSourceControlled())
{
// Already checked out or opened for add... or not in the depot at all
bSuccessfullyCheckedOut = true;
}
else if(SourceControlState->CanCheckout() || SourceControlState->IsCheckedOutOther())
{
bSuccessfullyCheckedOut = (SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), FilesToBeCheckedOut) == ECommandResult::Succeeded);
if (!bSuccessfullyCheckedOut)
{
OutFailReason = LOCTEXT("SCCCheckoutFailed", "Failed to check out the project file.");
}
}
else if(!SourceControlState->IsCurrent())
{
OutFailReason = LOCTEXT("SCCNotCurrent", "The project file is not at head revision.");
}
}
return bSuccessfullyCheckedOut;
}
FString GameProjectUtils::GetDefaultProjectTemplateFilename()
{
return TEXT("");
}
void GameProjectUtils::GetProjectCodeFilenames(TArray<FString>& OutProjectCodeFilenames)
{
IFileManager::Get().FindFilesRecursive(OutProjectCodeFilenames, *FPaths::GameSourceDir(), TEXT("*.h"), true, false, false);
IFileManager::Get().FindFilesRecursive(OutProjectCodeFilenames, *FPaths::GameSourceDir(), TEXT("*.cpp"), true, false, false);
}
int32 GameProjectUtils::GetProjectCodeFileCount()
{
TArray<FString> Filenames;
GetProjectCodeFilenames(Filenames);
return Filenames.Num();
}
void GameProjectUtils::GetProjectSourceDirectoryInfo(int32& OutNumCodeFiles, int64& OutDirectorySize)
{
TArray<FString> Filenames;
GetProjectCodeFilenames(Filenames);
OutNumCodeFiles = Filenames.Num();
OutDirectorySize = 0;
for (const auto& filename : Filenames)
{
OutDirectorySize += IFileManager::Get().FileSize(*filename);
}
}
bool GameProjectUtils::ProjectHasCodeFiles()
{
return GameProjectUtils::GetProjectCodeFileCount() > 0;
}
bool GameProjectUtils::ProjectRequiresBuild(const FName InPlatformInfoName)
{
// early out on projects with code files
if (ProjectHasCodeFiles())
{
return true;
}
bool bRequiresBuild = false;
if (!FApp::IsEngineInstalled())
{
// check to see if the default build settings have changed
bRequiresBuild |= !HasDefaultBuildSettings(InPlatformInfoName);
}
// check to see if any plugins beyond the defaults have been enabled
bRequiresBuild |= IProjectManager::Get().IsNonDefaultPluginEnabled();
return bRequiresBuild;
}
bool GameProjectUtils::DoProjectSettingsMatchDefault(const FString& InPlatformName, const FString& InSection, const TArray<FString>* InBoolKeys, const TArray<FString>* InIntKeys, const TArray<FString>* InStringKeys)
{
FConfigFile ProjIni;
FConfigFile DefaultIni;
FConfigCacheIni::LoadLocalIniFile(ProjIni, TEXT("Engine"), true, *InPlatformName, true);
FConfigCacheIni::LoadExternalIniFile(DefaultIni, TEXT("Engine"), *FPaths::EngineConfigDir(), *FPaths::EngineConfigDir(), true, NULL, true);
if (InBoolKeys != NULL)
{
for (int Index = 0; Index < InBoolKeys->Num(); ++Index)
{
FString Default(TEXT("False")), Project(TEXT("False"));
DefaultIni.GetString(*InSection, *((*InBoolKeys)[Index]), Default);
ProjIni.GetString(*InSection, *((*InBoolKeys)[Index]), Project);
if (Default.Compare(Project, ESearchCase::IgnoreCase))
{
return false;
}
}
}
if (InIntKeys != NULL)
{
for (int Index = 0; Index < InIntKeys->Num(); ++Index)
{
int64 Default(0), Project(0);
DefaultIni.GetInt64(*InSection, *((*InIntKeys)[Index]), Default);
ProjIni.GetInt64(*InSection, *((*InIntKeys)[Index]), Project);
if (Default != Project)
{
return false;
}
}
}
if (InStringKeys != NULL)
{
for (int Index = 0; Index < InStringKeys->Num(); ++Index)
{
FString Default(TEXT("False")), Project(TEXT("False"));
DefaultIni.GetString(*InSection, *((*InStringKeys)[Index]), Default);
ProjIni.GetString(*InSection, *((*InStringKeys)[Index]), Project);
if (Default.Compare(Project, ESearchCase::IgnoreCase))
{
return false;
}
}
}
return true;
}
bool GameProjectUtils::HasDefaultBuildSettings(const FName InPlatformInfoName)
{
// first check default build settings for all platforms
TArray<FString> BoolKeys, IntKeys, StringKeys, BuildKeys;
BuildKeys.Add(TEXT("bCompileApex")); BuildKeys.Add(TEXT("bCompileBox2D")); BuildKeys.Add(TEXT("bCompileICU"));
BuildKeys.Add(TEXT("bCompileSimplygon")); BuildKeys.Add(TEXT("bCompileSimplygonSSF")); BuildKeys.Add(TEXT("bCompileLeanAndMeanUE"));
BuildKeys.Add(TEXT("bIncludeADO")); BuildKeys.Add(TEXT("bCompileRecast")); BuildKeys.Add(TEXT("bCompileSpeedTree"));
BuildKeys.Add(TEXT("bCompileWithPluginSupport")); BuildKeys.Add(TEXT("bCompilePhysXVehicle")); BuildKeys.Add(TEXT("bCompileFreeType"));
BuildKeys.Add(TEXT("bCompileForSize")); BuildKeys.Add(TEXT("bCompileCEF3"));
const PlatformInfo::FPlatformInfo* const PlatInfo = PlatformInfo::FindPlatformInfo(InPlatformInfoName);
check(PlatInfo);
if (!DoProjectSettingsMatchDefault(PlatInfo->TargetPlatformName.ToString(), TEXT("/Script/BuildSettings.BuildSettings"), &BuildKeys))
{
return false;
}
if (PlatInfo->SDKStatus == PlatformInfo::EPlatformSDKStatus::Installed)
{
const ITargetPlatform* const Platform = GetTargetPlatformManager()->FindTargetPlatform(PlatInfo->TargetPlatformName.ToString());
if (Platform)
{
FString PlatformSection;
Platform->GetBuildProjectSettingKeys(PlatformSection, BoolKeys, IntKeys, StringKeys);
return DoProjectSettingsMatchDefault(PlatInfo->TargetPlatformName.ToString(), PlatformSection, &BoolKeys, &IntKeys, &StringKeys);
}
}
return true;
}
TArray<FString> GameProjectUtils::GetRequiredAdditionalDependencies(const FNewClassInfo& ClassInfo)
{
TArray<FString> Out;
switch (ClassInfo.ClassType)
{
case FNewClassInfo::EClassType::SlateWidget:
case FNewClassInfo::EClassType::SlateWidgetStyle:
Out.Reserve(2);
Out.Add(TEXT("Slate"));
Out.Add(TEXT("SlateCore"));
break;
case FNewClassInfo::EClassType::UObject:
auto ClassPackageName = ClassInfo.BaseClass->GetOutermost()->GetFName().ToString();
checkf(ClassPackageName.StartsWith(TEXT("/Script/")), TEXT("Class outermost should start with /Script/"));
Out.Add(ClassPackageName.Mid(8)); // Skip the /Script/ prefix.
break;
}
return Out;
}
GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject_Internal(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet<FString>& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason)
{
if ( !ParentClassInfo.IsSet() )
{
OutFailReason = LOCTEXT("MissingParentClass", "You must specify a parent class");
return EAddCodeToProjectResult::InvalidInput;
}
const FString CleanClassName = ParentClassInfo.GetCleanClassName(NewClassName);
const FString FinalClassName = ParentClassInfo.GetFinalClassName(NewClassName);
if (!IsValidClassNameForCreation(FinalClassName, ModuleInfo, DisallowedHeaderNames, OutFailReason))
{
return EAddCodeToProjectResult::InvalidInput;
}
if ( !FApp::HasGameName() )
{
OutFailReason = LOCTEXT("AddCodeToProject_NoGameName", "You can not add code because you have not loaded a project.");
return EAddCodeToProjectResult::FailedToAddCode;
}
FString NewHeaderPath;
FString NewCppPath;
if ( !CalculateSourcePaths(NewClassPath, ModuleInfo, NewHeaderPath, NewCppPath, &OutFailReason) )
{
return EAddCodeToProjectResult::FailedToAddCode;
}
FScopedSlowTask SlowTask( 7, LOCTEXT( "AddingCodeToProject", "Adding code to project..." ) );
SlowTask.MakeDialog();
SlowTask.EnterProgressFrame();
auto RequiredDependencies = GetRequiredAdditionalDependencies(ParentClassInfo);
RequiredDependencies.Remove(ModuleInfo.ModuleName);
// Update project file if needed.
auto bUpdateProjectModules = false;
// If the project does not already contain code, add the primary game module
TArray<FString> CreatedFiles;
TArray<FString> StartupModuleNames;
const bool bProjectHadCodeFiles = ProjectHasCodeFiles();
if (!bProjectHadCodeFiles)
{
// We always add the basic source code to the root directory, not the potential sub-directory provided by NewClassPath
const FString SourceDir = FPaths::GameSourceDir().LeftChop(1); // Trim the trailing /
// Assuming the game name is the same as the primary game module name
const FString GameModuleName = FApp::GetGameName();
if ( GenerateBasicSourceCode(SourceDir, GameModuleName, FPaths::GameDir(), StartupModuleNames, CreatedFiles, OutFailReason) )
{
bUpdateProjectModules = true;
}
else
{
DeleteCreatedFiles(SourceDir, CreatedFiles);
return EAddCodeToProjectResult::FailedToAddCode;
}
}
if (RequiredDependencies.Num() > 0 || bUpdateProjectModules)
{
UpdateProject(
FProjectDescriptorModifier::CreateLambda(
[&StartupModuleNames, &RequiredDependencies, &ModuleInfo, bUpdateProjectModules](FProjectDescriptor& Descriptor)
{
bool bNeedsUpdate = false;
bNeedsUpdate |= UpdateStartupModuleNames(Descriptor, bUpdateProjectModules ? &StartupModuleNames : nullptr);
bNeedsUpdate |= UpdateRequiredAdditionalDependencies(Descriptor, RequiredDependencies, ModuleInfo.ModuleName);
return bNeedsUpdate;
}));
}
SlowTask.EnterProgressFrame();
// Class Header File
const FString NewHeaderFilename = NewHeaderPath / ParentClassInfo.GetHeaderFilename(NewClassName);
{
FString UnusedSyncLocation;
TArray<FString> ClassSpecifiers;
// Set UCLASS() specifiers based on parent class type. Currently, only UInterface uses this.
if (ParentClassInfo.ClassType == FNewClassInfo::EClassType::UInterface)
{
ClassSpecifiers.Add(TEXT("MinimalAPI"));
}
if ( GenerateClassHeaderFile(NewHeaderFilename, CleanClassName, ParentClassInfo, ClassSpecifiers, TEXT(""), TEXT(""), UnusedSyncLocation, ModuleInfo, false, OutFailReason) )
{
CreatedFiles.Add(NewHeaderFilename);
}
else
{
DeleteCreatedFiles(NewHeaderPath, CreatedFiles);
return EAddCodeToProjectResult::FailedToAddCode;
}
}
SlowTask.EnterProgressFrame();
// Class CPP file
const FString NewCppFilename = NewCppPath / ParentClassInfo.GetSourceFilename(NewClassName);
{
FString UnusedSyncLocation;
if ( GenerateClassCPPFile(NewCppFilename, CleanClassName, ParentClassInfo, TArray<FString>(), TArray<FString>(), TEXT(""), UnusedSyncLocation, ModuleInfo, OutFailReason) )
{
CreatedFiles.Add(NewCppFilename);
}
else
{
DeleteCreatedFiles(NewCppPath, CreatedFiles);
return EAddCodeToProjectResult::FailedToAddCode;
}
}
SlowTask.EnterProgressFrame();
TArray<FString> CreatedFilesForExternalAppRead;
CreatedFilesForExternalAppRead.Reserve(CreatedFiles.Num());
for (const FString& CreatedFile : CreatedFiles)
{
CreatedFilesForExternalAppRead.Add( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*CreatedFile) );
}
bool bGenerateProjectFiles = true;
// First see if we can avoid a full generation by adding the new files to an already open project
if ( bProjectHadCodeFiles && FSourceCodeNavigation::AddSourceFiles(CreatedFilesForExternalAppRead) )
{
// We successfully added the new files to the solution, but we still need to run UBT with -gather to update any UBT makefiles
if ( FDesktopPlatformModule::Get()->InvalidateMakefiles(FPaths::RootDir(), FPaths::GetProjectFilePath(), GWarn) )
{
// We managed the gather, so we can skip running the full generate
bGenerateProjectFiles = false;
}
}
if ( bGenerateProjectFiles )
{
// Generate project files if we happen to be using a project file.
if ( !FDesktopPlatformModule::Get()->GenerateProjectFiles(FPaths::RootDir(), FPaths::GetProjectFilePath(), GWarn) )
{
OutFailReason = LOCTEXT("FailedToGenerateProjectFiles", "Failed to generate project files.");
return EAddCodeToProjectResult::FailedToHotReload;
}
}
SlowTask.EnterProgressFrame();
// Mark the files for add in SCC
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable() )
{
SourceControlProvider.Execute(ISourceControlOperation::Create<FMarkForAdd>(), CreatedFilesForExternalAppRead);
}
SlowTask.EnterProgressFrame( 1.0f, LOCTEXT("CompilingCPlusPlusCode", "Compiling new C++ code. Please wait..."));
OutHeaderFilePath = NewHeaderFilename;
OutCppFilePath = NewCppFilename;
if (!bProjectHadCodeFiles)
{
// This is the first time we add code to this project so compile its game DLL
const FString GameModuleName = FApp::GetGameName();
check(ModuleInfo.ModuleName == GameModuleName);
IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked<IHotReloadInterface>("HotReload");
const bool bReloadAfterCompiling = true;
const bool bForceCodeProject = true;
const bool bFailIfGeneratedCodeChanges = false;
if (!HotReloadSupport.RecompileModule(*GameModuleName, bReloadAfterCompiling, *GWarn, bFailIfGeneratedCodeChanges, bForceCodeProject))
{
OutFailReason = LOCTEXT("FailedToCompileNewGameModule", "Failed to compile newly created game module.");
return EAddCodeToProjectResult::FailedToHotReload;
}
// Notify that we've created a brand new module
FSourceCodeNavigation::AccessOnNewModuleAdded().Broadcast(*GameModuleName);
}
else if (GetDefault<UEditorPerProjectUserSettings>()->bAutomaticallyHotReloadNewClasses)
{
FModuleStatus ModuleStatus;
const FName ModuleFName = *ModuleInfo.ModuleName;
if (ensure(FModuleManager::Get().QueryModule(ModuleFName, ModuleStatus)))
{
// Compile the module that the class was added to so that the newly added class with appear in the Content Browser
TArray<UPackage*> PackagesToRebind;
if (ModuleStatus.bIsLoaded)
{
const bool bIsHotReloadable = FModuleManager::Get().DoesLoadedModuleHaveUObjects(ModuleFName);
if (bIsHotReloadable)
{
// Is there a UPackage with the same name as this module?
const FString PotentialPackageName = FString(TEXT("/Script/")) + ModuleInfo.ModuleName;
UPackage* Package = FindPackage(nullptr, *PotentialPackageName);
if (Package)
{
PackagesToRebind.Add(Package);
}
}
}
IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked<IHotReloadInterface>("HotReload");
if (PackagesToRebind.Num() > 0)
{
// Perform a hot reload
const bool bWaitForCompletion = true;
ECompilationResult::Type CompilationResult = HotReloadSupport.RebindPackages( PackagesToRebind, TArray<FName>(), bWaitForCompletion, *GWarn );
if( CompilationResult != ECompilationResult::Succeeded && CompilationResult != ECompilationResult::UpToDate )
{
OutFailReason = FText::Format(LOCTEXT("FailedToHotReloadModuleFmt", "Failed to automatically hot reload the '{0}' module."), FText::FromString(ModuleInfo.ModuleName));
return EAddCodeToProjectResult::FailedToHotReload;
}
}
else
{
// Perform a regular unload, then reload
const bool bReloadAfterRecompile = true;
const bool bForceCodeProject = false;
const bool bFailIfGeneratedCodeChanges = true;
if (!HotReloadSupport.RecompileModule(ModuleFName, bReloadAfterRecompile, *GWarn, bFailIfGeneratedCodeChanges, bForceCodeProject))
{
OutFailReason = FText::Format(LOCTEXT("FailedToCompileModuleFmt", "Failed to automatically compile the '{0}' module."), FText::FromString(ModuleInfo.ModuleName));
return EAddCodeToProjectResult::FailedToHotReload;
}
}
}
}
return EAddCodeToProjectResult::Succeeded;
}
bool GameProjectUtils::FindSourceFileInProject(const FString& InFilename, const FString& InSearchPath, FString& OutPath)
{
TArray<FString> Filenames;
IFileManager::Get().FindFilesRecursive(Filenames, *InSearchPath, *InFilename, true, false, false);
if(Filenames.Num())
{
// Assume it's the first match (we should really only find a single file with a given name within a project anyway)
OutPath = Filenames[0];
return true;
}
return false;
}
void GameProjectUtils::HarvestCursorSyncLocation( FString& FinalOutput, FString& OutSyncLocation )
{
OutSyncLocation.Empty();
// Determine the cursor focus location if this file will by synced after creation
TArray<FString> Lines;
FinalOutput.ParseIntoArray( Lines, TEXT( "\n" ), false );
for( int32 LineIdx = 0; LineIdx < Lines.Num(); ++LineIdx )
{
const FString& Line = Lines[ LineIdx ];
int32 CharLoc = Line.Find( TEXT( "%CURSORFOCUSLOCATION%" ) );
if( CharLoc != INDEX_NONE )
{
// Found the sync marker
OutSyncLocation = FString::Printf( TEXT( "%d:%d" ), LineIdx + 1, CharLoc + 1 );
break;
}
}
// If we did not find the sync location, just sync to the top of the file
if( OutSyncLocation.IsEmpty() )
{
OutSyncLocation = TEXT( "1:1" );
}
// Now remove the cursor focus marker
FinalOutput = FinalOutput.Replace(TEXT("%CURSORFOCUSLOCATION%"), TEXT(""), ESearchCase::CaseSensitive);
}
bool GameProjectUtils::InsertFeaturePacksIntoINIFile(const FProjectInformation& InProjectInfo, FText& OutFailReason)
{
const FString ProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile);
const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
const FString ProjectConfigPath = DestFolder / TEXT("Config");
const FString IniFilename = ProjectConfigPath / TEXT("DefaultGame.ini");
TArray<FString> PackList;
// First the starter content
if (InProjectInfo.bCopyStarterContent)
{
FString StarterPack;
if (InProjectInfo.TargetedHardware == EHardwareClass::Mobile)
{
StarterPack = TEXT("InsertPack=(PackSource=\"MobileStarterContent") + DefaultFeaturePackExtension + TEXT(",PackName=\"StarterContent\")");
}
else
{
StarterPack = TEXT("InsertPack=(PackSource=\"StarterContent") + DefaultFeaturePackExtension + TEXT(",PackName=\"StarterContent\")");
}
PackList.Add(StarterPack);
}
if (PackList.Num() != 0)
{
FString FileOutput;
if(FPaths::FileExists(IniFilename) && !FFileHelper::LoadFileToString(FileOutput, *IniFilename))
{
OutFailReason = LOCTEXT("FailedToReadIni", "Could not read INI file to insert feature packs");
return false;
}
FileOutput += LINE_TERMINATOR;
FileOutput += TEXT("[StartupActions]");
FileOutput += LINE_TERMINATOR;
FileOutput += TEXT("bAddPacks=True");
FileOutput += LINE_TERMINATOR;
for (int32 iLine = 0; iLine < PackList.Num(); ++iLine)
{
FileOutput += PackList[iLine] + LINE_TERMINATOR;
}
if (!FFileHelper::SaveStringToFile(FileOutput, *IniFilename))
{
OutFailReason = LOCTEXT("FailedToWriteIni", "Could not write INI file to insert feature packs");
return false;
}
}
return true;
}
bool GameProjectUtils::AddSharedContentToProject(const FProjectInformation &InProjectInfo, TArray<FString> &CreatedFiles, FText& OutFailReason)
{
//const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile);
const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
const FString ProjectConfigPath = DestFolder / TEXT("Config");
const FString IniFilename = ProjectConfigPath / TEXT("DefaultGame.ini");
// Now any packs specified in the template def.
UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder);
if (TemplateDefs != NULL)
{
EFeaturePackDetailLevel RequiredDetail = EFeaturePackDetailLevel::High;
if (InProjectInfo.TargetedHardware == EHardwareClass::Mobile)
{
RequiredDetail = EFeaturePackDetailLevel::Standard;
}
TUniquePtr<FFeaturePackContentSource> TempFeaturePack = MakeUnique<FFeaturePackContentSource>();
bool bCopied = TempFeaturePack->InsertAdditionalResources(TemplateDefs->SharedContentPacks,RequiredDetail, DestFolder,CreatedFiles);
if( bCopied == false )
{
FFormatNamedArguments Args;
Args.Add(TEXT("TemplateName"), FText::FromString(SrcFolder));
OutFailReason = FText::Format(LOCTEXT("SharedResourceError", "Error adding shared resources for '{TemplateName}'."), Args);
return false;
}
}
return true;
}
#undef LOCTEXT_NAMESPACE