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

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

Change 3297108 on 2017/02/10 by Mieszko.Zielinski

	Added AISight's peripherial vision angle claming as well as marked up UI to not allow values from outside of [0,180] range #UE4

	#jira UE-41114

Change 3299467 on 2017/02/13 by Marc.Audy

	Don't try to update active sounds on audio thread if the audio component is not active. If these functions are callled from a constructor on an async loading thread it can cause a crash

Change 3300692 on 2017/02/13 by Marc.Audy

	no auto

Change 3301424 on 2017/02/14 by Marc.Audy

	Handle gateway expansion before the node matching loop
	#jira UE-41858

Change 3301547 on 2017/02/14 by Marc.Audy

	PR #3246: Added BindDelegate that supports functions with custom (static) arguments (Contributed by PhoenixBlack)
	#jira UE-41926

Change 3301557 on 2017/02/14 by Marc.Audy

	When passing null to Rename for the new name, maintain the OldName is possible
	#jira UE-41937

Change 3301676 on 2017/02/14 by Marc.Audy

	Fix pending occlusion async traces from crashing during shutdown
	#jira UE-41939

Change 3302705 on 2017/02/14 by Mieszko.Zielinski

	Removed 'PRAGMA_DISABLE_OPTIMIZATION' uccurences from AIModule #UE4

Change 3302898 on 2017/02/14 by Dan.Oconnor

	Fix double negative

Change 3302954 on 2017/02/14 by Dan.Oconnor

	Make sure we use a good version of the class

Change 3302977 on 2017/02/14 by Dan.Oconnor

	Optimization in reinstancer turned back on - 3302898 has fixed the regression

Change 3302984 on 2017/02/14 by Dan.Oconnor

	Relink classes that were not recompiled in a wave of the compilation manager - currently only happens for data only blueprints.

	This fixes issues in Odin when using the compilation  manager

Change 3303824 on 2017/02/15 by Richard.Hinckley

	Updating URL for FABRIK system information.

Change 3304284 on 2017/02/15 by Dan.Oconnor

	Build fix

Change 3304297 on 2017/02/15 by Dan.Oconnor

	Shadow variable fix

Change 3304465 on 2017/02/15 by Lukasz.Furman

	fixed handling pathfollowing's requests by FloatingPawnMovement
	#jira UE-41884

Change 3305031 on 2017/02/15 by Marc.Audy

	All objects should get PostLoadSubobjects calls, regardless of whether they are outered to a CDO or not
	#jira UE-41708

Change 3305505 on 2017/02/15 by Michael.Noland

	Blueprints: Fix a crash when opening a BP with a parent class that no longer exists (unguarded access to the parent class)

Change 3305506 on 2017/02/15 by Michael.Noland

	QAGame: Created some assets that reference a non-existent type to test 'gracefully' handling missing native class types

Change 3306091 on 2017/02/16 by Marc.Audy

	PR #3263: Fixed duplicate comment from OnAudioFinished (Contributed by FrostByteGER)
	#jira UE-42027

Change 3306574 on 2017/02/16 by Marc.Audy

	Linked To pins can belong to invalid nodes and fail to load, this shouldn't be considered fatal

Change 3307160 on 2017/02/16 by Marc.Audy

	Rename(null, null ... is sometimes used to just force a name out of the way, so in that case don't try and maintain old name.

Change 3307982 on 2017/02/16 by Michael.Noland

	QAGame: Added another test asset for missing classes (this time a missing node class placed in a BP)

Change 3308097 on 2017/02/16 by Michael.Noland

	Graph Editor: Instantly clear GraphNodeForMenu and GraphPinForMenu as soon as the menu is dismissed, fixing crashes and other odd issues after deleting pins
	#jira UE-41789

Change 3308303 on 2017/02/16 by Dan.Oconnor

	Make sure we don't call GetDefaultObject while compiling on a non-native class

Change 3308850 on 2017/02/17 by Mieszko.Zielinski

	Fully exposed NavModifierVolume as ENGINE_API #UE4

Change 3309624 on 2017/02/17 by Phillip.Kavan

	[UE-40443] Recursively emit ctor initialization code for nested default subobjects when nativizing a Blueprint class.

	change summary:
	- modified FEmitDefaultValueHelper::OuterGenerate() to recursively detect and handle nested default subobjects.

	#jira UE-40443

Change 3310475 on 2017/02/17 by Dan.Oconnor

	Split bluepint compilation into CompileClassLayout and CompileFunctions, fix class hierarchy after creating reinstancers in blueprintcompilation manager. Together this means we don't need to run RecompileBlueprintBytecode

Change 3310487 on 2017/02/17 by Dan.Oconnor

	Fix build error missed by preflgiht

Change 3310497 on 2017/02/17 by Dan.Oconnor

	More build fixes for things missed by preflight...

Change 3310635 on 2017/02/17 by Dan.Oconnor

	Remove unused parameter and add comment to blueprint compilation manager explaining abuse of bBeingCompiled

Change 3310639 on 2017/02/17 by Dan.Oconnor

	Shadow variable fixes, not sure why these are being detected now

Change 3311855 on 2017/02/20 by Marc.Audy

	Fix UChildActorComponent::ParentComponent being null on the client
	#jira UE-42140

Change 3312444 on 2017/02/20 by Marc.Audy

	Add a bAutoDestroy pin to BP Spawn Sound and Force Feedback nodes to allow users to reuse the created component
	#jira UE-41267

Change 3312691 on 2017/02/20 by mason.seay

	Deleting map now that bug has been fixed

Change 3312709 on 2017/02/20 by Phillip.Kavan

	[UE-39705] Fix broken collision shapes when cooking with optimized BP component data option.

	change summary:
	- modified FComponentInstancingDataUtils::RecursivePropertyGather() to exclude deprecated properties, since they won't be serialized on save.
	- modified FBlueprintCookedComponentInstancingData::LoadCachedPropertyDataForSerialization() to remove the PPF_UseDeprecatedProperties flag (these are no longer included in the delta).
	- modified UBlueprintGeneratedClass::CheckAndApplyComponentTemplateOverrides() to remove the PPF_UseDeprecatedProperties flag (was being incorrectly used here).
	- modified AActor::CreateComponentFromTemplateData() to remove the PPF_UseDeprecatedProperties flag (was being incorrectly used here; this caused deprecated property defaults to be copied out to the instance).
	- modified AActor::CreateComponentFromTemplateData() to append RF_PostLoad/RF_NeedPostLoadSubobjects and call PostDuplicate()/ConditionalPostLoad() on the new instance (needed to mirror what SDO does in the unoptimized case - for proper physics RB setup specifically, but may be other areas where that's needed).

	#jira UE-39705

Change 3313161 on 2017/02/20 by Mieszko.Zielinski

	PR #3272: Use Pawn for GetNavAgentPropertiesRef(). (Contributed by drelidan7)

Change 3314151 on 2017/02/21 by Mieszko.Zielinski

	fix to hlods complaining about missing nav collision in cooked builds #UE4

	Made sure hlod-generated StaticMeshes are marked as not having navigation data

	#jira UE-42034

Change 3314355 on 2017/02/21 by Marc.Audy

	Set error message back to be correctly about mobility
	#jira UE-42209

Change 3314566 on 2017/02/21 by Phillip.Kavan

	[UE-40801] Switch to an ensure() to potentially help diagnose a one-off assertion crash in the SCS editor if encountered again in a future release.

	#jira UE-40801

Change 3315459 on 2017/02/21 by Mike.Beach

	Updated marquee selection in graph editors. Ctrl dragging now inverts nodes' selection state (not only deselects them - holding alt is now for only deselection).

	#jira UE-16359

Change 3315546 on 2017/02/21 by Mike.Beach

	Mirroring CL 3294552

	Count "GeneratedStub" as a success for cooked file generation - ensures the saved asset gets recorded in the asset registry.

	#jira ODIN-5869

Change 3315554 on 2017/02/21 by Mike.Beach

	Do not generate NativizedAsset plugin files if no Blueprints were nativized (cut down on mod generate/cook time).

	#jira ODIN-6211

Change 3317225 on 2017/02/22 by mason.seay

	Enable Net Use Owner Frequency on blueprints.  This allows the client to use different weapons. Doesn't fix UE-42017 though.

Change 3317495 on 2017/02/22 by Marc.Audy

	Expose raw input device configurations to other modules by request

	#jira UE-42204

Change 3319966 on 2017/02/23 by Nick.Atamas

	Polished up the material reroute node:
	 - Removed some unnecessary widgets
	 - Centered the pin node

Change 3320099 on 2017/02/23 by Mike.Beach

	Guarding against passing self pins to referance parameters (it is not a property that is referencable, and would crash). Notifying the user through pin connection messages, and providing a script exception.

	#jira UE-40861

Change 3321227 on 2017/02/24 by Marc.Audy

	Just use name rather than going Name -> String -> TCHAR -> Name

Change 3321425 on 2017/02/24 by Marc.Audy

	Minor optimizations to avoid string construction when doing StaticFindObject and ResolveName

Change 3321630 on 2017/02/24 by Mike.Beach

	Removing reference notation from actor pointer param - allowing you to pass 'self' to Blueprint exposed function.

Change 3321845 on 2017/02/24 by Lukasz.Furman

	fixed navlink processor trace accepting only components with WorldStatic object type
	#ue4

Change 3322474 on 2017/02/24 by Aaron.McLeran

	UE-42345 Rewriting thumbnail renderer

Change 3322490 on 2017/02/24 by Aaron.McLeran

	UE-42345  Forgot to take abs of sample before averaging

Change 3323562 on 2017/02/27 by Mike.Beach

	Fixing bad merge, copying loop from //UE4/Main that accidently got replaced.

Change 3323685 on 2017/02/27 by Mike.Beach

	Preventing us from cross-binding editor & PIE actors when we fixup level script actor bindings (on duplicate for PIE).

	#jira UE-30816

Change 3323776 on 2017/02/27 by Marc.Audy

	Coding standard clean up pass

Change 3324050 on 2017/02/27 by Ben.Zeigler

	Fix issue with a StreamableHandle being cancelled while in progress leaving the in progress flag active. Added and improved error messages when streaming goes wrong
	Port of 3317217, 3315540, and 3314374 from UE4-Fortnite

Change 3324294 on 2017/02/27 by Ben.Zeigler

	Engine changes needed to support "Asset Management" UI:
	Add concept of "Manage" dependency to the Asset Registry, to represent that an asset like a texture is managed by a Primary Asset. This will be used to compute usage statistics and chunking
	Add ability for AssetManager to override the PrimaryAssetType/Id on a asset data loaded off disk. Needed so the asset audit tools work properly
	Significant performance improvements to the asset registry dependency gather, and correctly report as in progress while dependencies are still being gathered. On Fortnite it now finishes in 10 seconds instead of 100
	Add bUpdateDiskCacheAfterLoad option for the asset registry, if true (default) this will update the Asset Registry's disk cache when an object is loaded, only in the editor. This is so changes made in PostLoad are correctly mirrored in the disk cache
	Add PrimaryAssetType as a wrapper struct around FName to allow customizations and blueprint usage, clean up the noexport definitions for a few related classes
	Add Asset Manager code to create and query "Manage" references used for auditing and chunking
	Add code to read AssetManager scanning rules out of the AssetManagerSettings object, also settable in editor
	Made it so UWorlds are now PrimaryAssets of the type Map, and enable the AssetManager by default for all games
	Port of CL #3323720 and related fixes from Fortnite

Change 3324295 on 2017/02/27 by Ben.Zeigler

	Add AssetManagerEditor which contains the editor interface for the AssetManager system, and engine code needed to support it
	Add support for Management references to the Reference Viewer, and add ability to extend that context menu from plugins/games
	Add struct customizations for PrimaryAssetId and PrimaryAssetType
	Add AssetAuditBrowser window that shows a specialized asset picker for auditing, accessible from content browser, reference viewer, and main windows pane
	Add AssetAuditContext, which is a cleaned up port of the one from Paragon. This needs some more work before being final
	Expose PropertyCustomizationHelpers::MakePropertyComboBox which allows making an "enum-like" combo box for struct customizations, it now works much like the PropertyEditorAsset UI
	Add Custom Column support to AssetView/AssetPicker. This can be used to show runtime-generated column data
	Fix bug in SAssetView where column view did not work with a filter predicate, because the column view was generated before the deferred filter predicate run, leading to an empty filter
	Port of CL #3323722 and related fixes from Fortnite

Change 3324398 on 2017/02/27 by Ben.Zeigler

	CIS fix

Change 3324442 on 2017/02/27 by Ben.Zeigler

	Nonunity fix discovered while testing my nonunity fix

Change 3325465 on 2017/02/28 by Marc.Audy

	Expand RawInput to support up to 20 buttons

Change 3325468 on 2017/02/28 by Marc.Audy

	Fix CIS

Change 3325887 on 2017/02/28 by Phillip.Kavan

	[UE-41893] Implicitly nativize child Blueprints that override one or more BlueprintCallable functions from a parent Blueprint.

	change summary:
	- added FBlueprintEditorUtils::ShouldNativizeImplicitly()
	- modified FBlueprintGlobalOptionsDetails::IsNativizeEnabled() to disable the 'Nativize' checkbox when the BP is implicitly enabled
	- modified FBlueprintGlobalOptionsDetails::GetNativizeState() to set the 'Checked' state when the BP is implicitly enabled
	- modified FBlueprintGlobalOptionsDetails::GetNativizeTooltip() to set an alternate tooltip for the disabled state (when the BP is implicitly enabled)
	- modified FBlueprintNativeCodeGenModule::IsTargetedForReplacement() to ensure that implicitly-enabled BPs are flagged as selected for nativization

	#jira UE-41893

Change 3326713 on 2017/02/28 by Marc.Audy

	Update MAX_NUM_CONTROLLER_BUTTONS to match number of keys created

Change 3327688 on 2017/03/01 by Marc.Audy

	Fix spelling, remove autos

Change 3328139 on 2017/03/01 by Marc.Audy

	Win32 doesn't report the DeviceData in the same way that Win64 does, removing filtered check for now so that Win32 packaged games can use RawInput (4.15.1)

	#jira UE-42375

Change 3328550 on 2017/03/01 by Mike.Beach

	Typo fix in cast node tooltip.

Change 3328575 on 2017/03/01 by Nicholas.Blackford

	Submitting Tick Interval Functional Test

Change 3328972 on 2017/03/02 by Jack.Porter

	Fix for crash entering Landscape mode

	#jira UE-42497

Change 3329224 on 2017/03/02 by Nick.Bullard

	Removing Redirector from EngineTest project

Change 3330093 on 2017/03/02 by Mike.Beach

	Modified fix from Marc.Audy - Guarding against malformed graphs (missing their schema), which can happen in the middle of an undo transaction (removing the graph). Returning the graph's path name in this situation (instead of the display name), so we atleast have some semblance of context.

	#jira UE-42166

Change 3330306 on 2017/03/02 by Mike.Beach

	Replacing ArrayLibrary Get() calls in blueprints with a custom node, which can be toggled back and forth from returning by reference or by value.

	#jira UE-6451

Change 3330626 on 2017/03/02 by samuel.proctor

	Functional Test for Blueprint Containers

Change 3330690 on 2017/03/02 by Mike.Beach

	Modified the fix from CL 3308097 - cannot clear the edgraph pin context since many menu actions expect it be available still as the menu is clossing (menu's dismiss gets triggered before the action is executed).

	#jira UE-42500

Change 3330704 on 2017/03/02 by Mike.Beach

	CIS fix - fallout from CL 3330306

Change 3330875 on 2017/03/02 by Dan.Oconnor

	Iteration on compile manager - removed skeleton compile pass in favor of FastGenerateSkeletonClass (directly generate reflection data from blueprint source data - no graph cloning)

Change 3330892 on 2017/03/02 by Mike.Beach

	CIS fix for linux builds - include filename is case sensitive.

Change 3331585 on 2017/03/03 by Mike.Beach

	Fix for CIS issues (fallout from CL 3330306) - had success/failure return value flipped. Spuriously failing on deprecated node fixup.

Change 3333455 on 2017/03/06 by Ben.Zeigler

	Cleaned up version of CL #3332060, fixes crashes when calling StreamableManager::SynchronousLoad from inside a async PostLoad callback
	Also disable the "do sync load as async load" code in EDL, as EDL basically already does that internally
	Move the recursion guard inside async load tick outside of the EDL section, it's just as unsafe with EDL off

Change 3333484 on 2017/03/06 by Ben.Zeigler

	#jira UE-42312 Fix crash trying to read Searchable Name references to objects in the same package, now guess at package/object name

Change 3333553 on 2017/03/06 by Ben.Zeigler

	#jira UE-42387 Don't write out empty generated ini files for config files that are empty in both source and destination, this stops plugins without configs from ending up in cache

Change 3333697 on 2017/03/06 by Mike.Beach

	Resolving some CIS errors - fix for missed handling of split-struct pins (fallout from CL 3330306) on deprecated node conversion (mapping old pins to new pins).

Change 3334047 on 2017/03/06 by Ben.Zeigler

	#jira UE-42587 Now that we handle Add gameplay cues correctly by deferring them until after load, we also need to handle Remove cues, to avoid cues being stuck on permanently.

Change 3334228 on 2017/03/06 by Ben.Zeigler

	#jira UE-42153 Fix several crashes with gameplay tag query structs
	#jira UE-39760 Fix it to display tag query description on creation

Change 3335221 on 2017/03/07 by Lukasz.Furman

	fixed compilation errors for macros: UE_VLOG_MESH, UE_CVLOG_MESH
	#ue4

Change 3335733 on 2017/03/07 by dan.reynolds

	Fixing Attenuation Shape Material Reference

Change 3335918 on 2017/03/07 by Mike.Beach

	More deeply nesting an active world check in UMeshComponent::CacheMaterialParameterNameIndices(). Only guarding the parts that use the world (prior to this, we were blocking material parameter discovery, which was causing cooked content to loose material settings).

	#jira UE-42480

Change 3336053 on 2017/03/07 by zack.letters

	Moved and renamed test to meet naming convention and proper location

Change 3336087 on 2017/03/07 by Phillip.Kavan

	[UE-18618] Fix an ensure() misfire on PIE exit for listen server mode.

	change summary:
	- Modified UWorld::TransferBlueprintDebugReferences() to allow the LevelScript BP's target debug object reference to be reset to NULL when CreatePIEWorldBySavingToTemp() has recompiled it during the PIE startup process and autosaved the BP as a temporary.

	#jira UE-18618

Change 3336118 on 2017/03/07 by Phillip.Kavan

	Ensure that BP class component templates are included as preload dependencies where appropriate.

Change 3336418 on 2017/03/07 by Marc.Audy

	Set the PIEInstanceID before calling ConvertToPIEPackageName (4.15.1)
	#jira UE-42507

Change 3336529 on 2017/03/07 by dan.reynolds

	AEOverview UMG Interface

Change 3336729 on 2017/03/07 by Michael.Noland

	Blueprints: Changed a checkSlow() followed by unguarded access to an if and ensure() in BlueprintActionFilterImpl::IsDeprecated to prevent a potential crash in release if the node class is invalid for some reason
	#jira UE-42519

Change 3337054 on 2017/03/08 by Mieszko.Zielinski

	Fixed UGameplayTaskResource::AutoResourceID getting cleared on hot reload #UE4

Change 3337605 on 2017/03/08 by Mieszko.Zielinski

	PR #3345: Fix reversed comparison in FGameplayResourceSet::HasAllIDs (Contributed by hoelzl)

Change 3337612 on 2017/03/08 by Lina.Halper

	Commenting out ensure as this doesn't cause any harm and fix it up later by itself.
	- adding ticket for further investigation

	#rb: Martin.Wilson
	#jira: UE-42062

Change 3338353 on 2017/03/08 by Mike.Beach

	Undoing CL 3320099, and instead allowing self nodes to be plugged into const ref inputs. Now auto-generating ref terms for the self node (the input param expects an addressable UProperty). Skipping this for native functions, as UHT already does something similar.

	#jira UE-40861

Change 3340052 on 2017/03/09 by Marc.Audy

	Don't mark a blueprint dirty if the default value isn't actually set
	#jira UE-42511

Change 3340211 on 2017/03/09 by samuel.proctor

	Adding TMap/TSet tests for Containers Functional Test

Change 3340272 on 2017/03/09 by Marc.Audy

	auto removals
	small optimizations

Change 3340341 on 2017/03/09 by Marc.Audy

	Fortnite fixes for blueprint exposed editor only struct members
	#jira UE-42430

Change 3340356 on 2017/03/09 by Marc.Audy

	Do not allow blueprint exposed editor only struct members
	#jira UE-42430

Change 3340369 on 2017/03/09 by Mike.Beach

	Certain operations expect set/map elements to be constructed, instead of using an 'uninitialized' value (like with FStrings, previously this would blow up attempting to assign a value to an FString that hadn't been constructed). Fix is to construct the member when we make space in the container (emulating execSetArray).

	#jira UE-42572

Change 3340445 on 2017/03/09 by mason.seay

	Renamed and updated test map.  Also disabled tests until reviewed

Change 3340627 on 2017/03/09 by Marc.Audy

	Remove autos

Change 3340639 on 2017/03/09 by Dan.Oconnor

	Avoid CDO creation when asking if an object IsDefaultSubobject

Change 3340642 on 2017/03/09 by Marc.Audy

	Correctly maintain removed items from arrays when duplicating actors via T3D
	#jira UE-42278

Change 3340689 on 2017/03/09 by Dan.Oconnor

	Avoid UObject::Modify calls when renaming edgraph nodes as part of UEdGraphNode::PostLoad() or UEdGraph::MoveNodesToAnotherGraph

Change 3340709 on 2017/03/09 by Dan.Oconnor

	Remove misplace dClassDefaultObject null check for now

Change 3340710 on 2017/03/09 by Dan.Oconnor

	Avoid FindRedirectedPropertyName when performing StaticDuplicateObject

Change 3340728 on 2017/03/09 by Dan.Oconnor

	Null checking CDO so that we can duplicate a class with no CDO

Change 3342184 on 2017/03/10 by mason.seay

	Nav mesh generation test - not finished

Change 3342930 on 2017/03/13 by Mieszko.Zielinski

	Added missing undefining of local macros in VisualLoggerAutomationTests.cpp #UE4

Change 3343739 on 2017/03/13 by Marc.Audy

	Protect against ChildActorClass becoming null while ChildActorTemplate remains valid.

Change 3343758 on 2017/03/13 by Marc.Audy

	Ensure that when you change visibility, children also get marked dirty as needed.
	SetVisibility is no longer virtual, use OnVisibilityChanged in subclasses instead
	#jira UE-42240

Change 3343816 on 2017/03/13 by Mike.Beach

	Making sure we build CrashReporter for nativized clients.

	#jira UE-42056

Change 3343858 on 2017/03/13 by Phillip.Kavan

	Back out changelist 3336118 (per discussion) - did not solve the issue.

Change 3344218 on 2017/03/13 by Mike.Beach

	Patching some holes in the wildcard pin logic for our new array GetItem node (making sure the node properly retains its type).

Change 3344388 on 2017/03/13 by Mike.Beach

	Preventing make/break nodes from being in the context menu for structs that are not labeled 'BlueprintType' (still available if you drag off a node with a struct pin of that type).

	#jira UE-37971

Change 3344411 on 2017/03/13 by dan.reynolds

	AEOverviewMain update

	- Organized Variables

	- Added comments on level interface with UI script

Change 3344956 on 2017/03/14 by Marc.Audy

	Remove autos
	Slight optimization

Change 3345365 on 2017/03/14 by Mike.Beach

	In the Blueprint diff tool, no longer assuming that graph names are unique (using the outer path to find matching graphs between diff panels).

	#jira UE-42787

Change 3345565 on 2017/03/14 by Marc.Audy

	auto removal

Change 3345654 on 2017/03/14 by Marc.Audy

	Allow hierarchical metadata querying when HACK_HEADER_GENERATION is true

Change 3345771 on 2017/03/14 by Zak.Middleton

	#ue4 - Refactored CharacterMovementComponent determination of net send rate when combining moves into a virtual function GetClientNetSendDeltaTime(). Added configurable values to GameNetworkManager under [/Script/Engine.GameNetworkManager]:

	ClientNetSendMoveDeltaTime=0.0111f
	ClientNetSendMoveDeltaTime=0.0222f
	ClientNetSendMoveThrottleAtNetSpeed = 10000
	ClientNetSendMoveThrottleOverPlayerCount=10

	These are the default values maintained for backwards compat.

	Related to OR-36422.

Change 3346314 on 2017/03/14 by Dan.Oconnor

	Add two features to FBlueprintCompileReinstancer. Exposing it's CPFUO extensions and add a flag to avoid potentially unneeded CDO duplication.

Change 3346329 on 2017/03/14 by Dan.Oconnor

	Avoid CDO creation in UBlueprintGeneratedClass::PostLoad - rely instead on compiler

Change 3346436 on 2017/03/14 by Dan.Oconnor

	Compilation Manager iteration - improvements to reinstancing logic and postponement of reinstancing reference replacement until after loading has finished (done strictly to reduce the number of 'find references' calls). Behavior change is behind the GMinimalCompileOnLoad flag

Change 3346632 on 2017/03/14 by Ben.Zeigler

	Change StringClassReference customization to use MustImplement and BlueprintBaseOnly metadata, to match the metadata used by SubclassOf customization
	Add missing Class Property metadata to the metadata list

Change 3347525 on 2017/03/15 by Marc.Audy

	PR #3371: Fix for binding ability action to input component (Contributed by ryanjon2040)
	#jira UE-42810

Change 3347562 on 2017/03/15 by Phillip.Kavan

	[UE-32816] Support for value-based bitfield enum associations in the editor.

	notes:
	- default mode is still index-based, so there are no backwards-compatibility issues

	change summary:
	- new metadata key for flagging enums as value-based (UseEnumValuesAsMaskValuesInEditor)
	- modified SPropertyEditorNumeric::Construct() to include logic for handling value-based enum associations
	- modified SGraphPinInteger::Construct() to include logic for handling value-based enum associations
	- added default value fixup to UK2Node_BitmaskLiteral, so that changed/removed values get masked out on load
	- switched UK2Node_BitmaskLiteral::PostLoad() to Serialize(), so that default value fixup occurs before compilation

	#jira UE-32816

Change 3348030 on 2017/03/15 by Marc.Audy

	Remove experimental blueprintable components setting, they are supported fully

Change 3348034 on 2017/03/15 by Phillip.Kavan

	CIS fix.

Change 3348054 on 2017/03/15 by Marc.Audy

	Fix shadow error

Change 3348063 on 2017/03/15 by mason.seay

	Updateed bp logic to use asserts.  Added scenarios to descriptions of tests

Change 3348131 on 2017/03/15 by mason.seay

	Updating maps and reorganizing content

Change 3348146 on 2017/03/15 by Mike.Beach

	Making it so we can use DataTable variables as inputs in the GetDataTableRow node. The output pin is now a wildcard when the row type is undefined, and we throw an access error at runtime if the table and output type don't match.

Change 3348213 on 2017/03/15 by dan.reynolds

	AEOverview UMG Update

	- Added level selection persistence between categories (so you can pick and choose from multiple categories)

	- Added a clear all selections button

	- Added comments to the UMG BP

Change 3348344 on 2017/03/15 by Lukasz.Furman

	fixed missing path following result flag descriptions
	#ue4

Change 3348489 on 2017/03/15 by mason.seay

	Moved content and updated test descriptions

Change 3348496 on 2017/03/15 by Mike.Beach

	Keeping the new version of the GetArrayItem node from causing a stack overflow with wildcard reroute nodes.

Change 3348502 on 2017/03/15 by Ben.Zeigler

	#jira UE-42935 Fix several issues with GameplayTag and Container switch nodes crashing. Container didn't handling having multiple empty nodes correctly
	Fix general issue with Switch nodes where removing an execution pin with right click was not synchronizing the pin list properly
	Change it so the Container switch shows the simple tag string instead of Case 0, and change it to not quote by default for Container display strings

Change 3348504 on 2017/03/15 by Ben.Zeigler

	#jira UE-41554 Add GameplayTag initialization to InitializeObjectReferences if it hasn't been initialized yet, this is important so it gets initialized before being initialized from unsafe areas like Serialize

Change 3348512 on 2017/03/15 by Mike.Beach

	Reroute nodes connected to a new output, will propagate the type through its inputs (was previously treating the input's wildcard type as authoritative).

Change 3348513 on 2017/03/15 by Phillip.Kavan

	[UE-38979] Error out on an attempt to nativize a Blueprint class that also implements a native C++ interface with a pure virtual function declaration.

	change summary:
	- added TIsAbstract<T> for traits testing to see if native C++ types are abstract (in terms of C++, not UE4)
	- changed TCppStructOps::IsAbstract() to use TIsAbstract<T>
	- added UClass::CppClassOps to capture class-specific traits info for the underlying C++ class type
	- modified UClass::PurgeClass() to clean up class-specific traits info (if valid)
	- modified FNativeClassHeaderGenerator::ExportNativeGeneratedInitCode() to generate code to initialize class-specific traits info for compiled-in class types
	- modified FBlueprintNativeCodeGenModule::IsTargetedForReplacement() to throw an error during nativization if a target BP class is found to implement a native interface class that's also abstract (i.e. an interface class that declares one or more of its methods as pure virtual)
	- modified BlueprintActionFilterImpl::IsExtraneousInterfaceCall() to initially exclude any native interface class that is also abstract
	- modified FKismetEditorUtilities::CanBlueprintImplementInterface() to additionally exclude any native class that is also abstract
	- modified FBlueprintInterfaceFilter::IsClassAllowed() to additionally exclude any native class that is also abstract

	#jira UE-38979

Change 3348651 on 2017/03/15 by Mike.Beach

	Fixing the new GetDataTableRow node so that it'll give you the option of reroute nodes.

Change 3348684 on 2017/03/15 by Michael.Noland

	Blueprints: Allow string and text variables to be marked as multi-line

	PR #3294: UE-42147: Add multiline to BP view details (Contributed by projectgheist)

	#jira UE-42275

Change 3348691 on 2017/03/15 by Michael.Noland

	Cameras: Added support for specifying a default aspect ratio and whether or not to constrain to it in a camera manager subclass; useful when using custom view logic that doesn't source from a camera component as the view target
	PR #2593: Finish implementing aspect ratio handling for PlayerCameraManager (Contributed by CleanCut)
	#jira UE-33052

Change 3348698 on 2017/03/15 by Michael.Noland

	Removed a sprite reference from trigger shape classes and excluded some component references from camera rigs in cooked builds
	PR #2922: Ensuring editor data is not accessed when excluded from cook (Contributed by moritz-wundke)

	#jira UE-38484

Change 3348722 on 2017/03/15 by Dan.Oconnor

	Fix replacement bug - due to last minute refactor of this reference replacer call

Change 3348736 on 2017/03/15 by Michael.Noland

	Blueprints: Added missing include for UTextProperty (compiled fine locally both with the file checked out and the file unmodified)

Change 3348810 on 2017/03/15 by Michael.Noland

	Blueprints: Added support for seeing the user defined tooltip on get/set nodes for local variables
	PR #3256: UE-41098: Added UFunction argument (Contributed by projectgheist)

Change 3348811 on 2017/03/15 by Michael.Noland

	PR #3380: Added CancelAbility Blueprint node (Contributed by ryanjon2040)

	#jira UE-42904

Change 3348969 on 2017/03/15 by Dan.Oconnor

	Build fix

Change 3349023 on 2017/03/16 by Aaron.McLeran

	Copying //Tasks/UE4/Private-GDC17-Audio to Dev-Framework (//UE4/Dev-Framework)

Change 3349389 on 2017/03/16 by mason.seay

	Finished up Navigation map.  Improved Navmesh map (still needs some work before review)

Change 3349575 on 2017/03/16 by Marc.Audy

	Emit ScriptMacros.h in addition to ObjectMacros.h in generated headers

Change 3349628 on 2017/03/16 by Ben.Zeigler

	Add direct support for Chunk setting to AssetManager. If AssetManager exists and no game callback is set it uses the new, much faster method. Otherwise it falls back to the old one
	Fix some memory corruption issues in ChunkManifestGenerator where it was modifying a map while iterating it, could lead to assets ending up in multiple chunks accidentally
	Remove the "Old Cooker" entirely, it hasn't functioned since around 4.9
	Various fixes to AssetManagerEditorModule
	Convert ShooterGame to use the AssetManager for chunking

Change 3349629 on 2017/03/16 by Ben.Zeigler

	Change Fortnite to use the AssetManager chunking system, which simplifies the chunk 1 setup significantly
	Also includes changes made on Fortnite Branch as  CL #3323724:
	Fortnite changes to take advantage of the Manage dependency in the asset manager
	Move definition of asset types to ini from native, and simplify it so all zone themes are scanned, even if not used
	Make FeedbackBank a primary asset type. It's currently editor only as there are some outdated banks we don't want to cook

Change 3350043 on 2017/03/16 by Marc.Audy

	Fix Audio compile errors

Change 3350092 on 2017/03/16 by Dan.Oconnor

	Fix missing output parameters when the function result node is pruned

Change 3350190 on 2017/03/16 by Ben.Zeigler

	CIS fix

Change 3350707 on 2017/03/16 by Dan.Oconnor

	Add means of enabling BlueprintCompilationManager via editor ini. Wedging the check into LaunchEngineLoop because of assets that are loaded during engine initialization

Change 3350820 on 2017/03/16 by Joe.Conley

	Xenakis project: Setting GameMode to GameMode instead of None so the game doesn't crash on Play

Change 3350893 on 2017/03/16 by Dan.Oconnor

	Build fix

Change 3351017 on 2017/03/16 by Dan.Oconnor

	Using ordered arguments instead of named arguments improves load time in BP heavy projects

Change 3351056 on 2017/03/16 by Dan.Oconnor

	Avoiding Copies

Change 3351062 on 2017/03/16 by Dan.Oconnor

	Enable BlueprintCompilationManager by default - this is a major change in code path when loading uassets that contain blueprints

Change 3351770 on 2017/03/17 by Marc.Audy

	Fix CIS warnings

Change 3351818 on 2017/03/17 by Mike.Beach

	CopyPropertiesForUnrelatedObjects() will now only copy tagged data when the two objects truly are unrelated (different native base classes). We have to do this because the two native base classes may have different serialization methods that add/expect different data, which is not compatible with the other.

	#jira UE-35970

Change 3351918 on 2017/03/17 by Mike.Beach

	CIS fix - renaming local so it doesn't conflict with the one in the outer scope.

Change 3351931 on 2017/03/17 by Ben.Zeigler

	Make CoreRedirects a proper Automated Test, and fix a test failure with not handling : in the output string
	Fix legitimate regression where doing a package -> package rename would clear Outer, this was a result of a fix I made in Main a few weeks ago

Change 3351956 on 2017/03/17 by Dan.Oconnor

	Make sure result element is emptied when calling Intersect, Union, or Difference
	#jira UE-42993

Change 3352049 on 2017/03/17 by Ben.Zeigler

	#Jira UE-42118 Add RemoveGameplayTag to the tag blueprint library
	Delete (with redirector) redundant AddGameplayTagToContainer function that got accidentally added in parallel on Fortnite. Decided to keep the shorter TagContainer parameter name for both though

Change 3352065 on 2017/03/17 by Aaron.McLeran

	Fixing compile errors

	- deleting unused files
	- removing #pragma once in SSynthKnob.cpp
	- Making phonon have win64 whitelist to avoid compiling on other platforms

Change 3352100 on 2017/03/17 by Aaron.McLeran

	Fixing compile errors

	- Moving header file to public folder since it's used outside of module

Change 3352182 on 2017/03/17 by Ben.Zeigler

	#jira UE-39815 Fix several issues with renaming tags in the tag settings view, it now deletes redirectors properly when renaming or making a new tag that matches an existing redirector

Change 3352286 on 2017/03/17 by Ben.Zeigler

	#jira UE-39519 Add error messages when only one of GameMode/GameState is derived from the outdated parent classes
	Modified version of PR #3285: Add error log messages if the GameMode/GameState are mis-matched (Contributed by jwatte)

Change 3352299 on 2017/03/17 by Ben.Zeigler

	#jira UE-40544
	PR #3130: UE-40544: Check pause state if state change is allowed (Contributed by projectgheist)

Change 3352303 on 2017/03/17 by Ben.Zeigler

	#jira UE-40856
	Commit PR #3147:  Remove unnecessary directory separator for GetSaveGamePath  (Contributed by projectgheist)
	Remove unnecessary FString casting and in OpenGLDebugFrameDump.cpp there were FString multiplications that would never compile

Change 3352320 on 2017/03/17 by Ben.Zeigler

	#jira UE-40087 Fix it so console keybind can be used in shipping games with console enabled
	Commit PR #3079: Fix ALLOW_CONSOLE define usage (Contributed by KrisRedbeard)

Change 3352338 on 2017/03/17 by Ben.Zeigler

	#jira UE-42800
	PR #3367: Made CheatManager more useful for non-FPShooters (Contributed by crumblycake)

Change 3352352 on 2017/03/17 by Dan.Oconnor

	Emptying map instead of trying to remove an element when conversion of a value type fails - can't remove a single element until the map is rehashed
	#jira UE-42937

Change 3352581 on 2017/03/17 by Lukasz.Furman

	fixed memory leak in navmesh generators
	copy of CL# 3352356
	#ue4

Change 3352665 on 2017/03/17 by Aaron.McLeran

	Fixing build error

	- Adding virtual destructor to FSoundWaveSoundWaveAssetActionExtender
	- Also renamed the class to only include SoundWave once!
	- Fixing static analysis warning on null deref.

Change 3352685 on 2017/03/17 by Dan.Oconnor

	Fix for bad behavior of GetValues and GetKeys functions when there are gaps in a TMap (e.g. due to Remove calls)
	#jira UE-42547

Change 3352706 on 2017/03/17 by Aaron.McLeran

	Fixing build error

	Changing TSharedPtr<FSoundWaveSoundWaveAssetActionExtender> to TSharedPtr<ISoundWaveAssetActionExtensions>

Change 3352708 on 2017/03/17 by Dan.Oconnor

	Data only and interface blueprints need SkeletonGeneratedClass set on load so that they can be used by the BlueprintEditor

	#jira UE-43023

Change 3352860 on 2017/03/17 by Lukasz.Furman

	fixed memory leak in navmesh generators
	copy of CL# 3352849
	#ue4

Change 3352967 on 2017/03/17 by Dan.Oconnor

	Avoid tagging blueprints as modified while compiling with the new compilation manager. Leaving old code path unaffected, although it may benefit from this change.

	#jira UE-43027

Change 3352979 on 2017/03/17 by Dan.Oconnor

	Static analysis driven fixes

	#jira UE-43044

Change 3352987 on 2017/03/17 by Aaron.McLeran

	Fixing build error

	- Removing myo from other platforms, win64 only

Change 3353234 on 2017/03/18 by Marc.Audy

	Fix Win32 build

Change 3353344 on 2017/03/19 by Marc.Audy

	Fix cyclic includes in new Audio code

Change 3353350 on 2017/03/19 by Marc.Audy

	Disable static analysis for myo third party code

Change 3353750 on 2017/03/20 by Marc.Audy

	Fix additional cyclic include

Change 3353926 on 2017/03/20 by Mieszko.Zielinski

	Made FNavAgentProperties::GetExtent return INVALID_NAVEXTENT if prop's AgentRadius is not set #UE4

	This results in using FNavAgentProperties::DefaultProperties in navigation system queries to fallback to default query extent.

	#jira UE-18493

Change 3354249 on 2017/03/20 by Mike.Beach

	Raising a UHT error if you use a non-byte enum type in a Blueprint function. Blueprints currently only support uint8 enums (already an error if you tag the enum with 'BlueprintType', this error just emulates/extends that one).

	#jira UE-42479

Change 3354464 on 2017/03/20 by Dan.Oconnor

	Fix missing source path when using compilation manager

Change 3354499 on 2017/03/20 by Dan.Oconnor

	Disable compilation manager

Change 3354620 on 2017/03/20 by Ben.Zeigler

	#jira UE-43087 Fix crash when calling HasGPUEmitter on a Server build, this is newly an issue because it is calling GetAssetRegistryTags in more places than it used to

Change 3354714 on 2017/03/20 by Michael.Noland

	PR #3352: Fixed issue with diffed Blueprints being searchable (Contributed by MichaelSchoell)

	#jira UE-42655

Change 3354718 on 2017/03/20 by Michael.Noland

	Engine: Change FViewport::IsGameRenderingEnabled to be static
	PR #3317: FViewport::IsGameRenderingEnabled (Contributed by tomix1024)

	#jira UE-42471

Change 3354721 on 2017/03/20 by Michael.Noland

	PR #3293: Made GetDefaultLocale accessible in blueprint (Contributed by derekvanvliet)

	#jira UE-42274

Change 3354907 on 2017/03/20 by Aaron.McLeran

	Fixing content in xenakis map

Change 3355223 on 2017/03/20 by Ben.Zeigler

	#jira UE-43096 Fix crash when trying to ResolveName a path that ends in . (apparently when you LoadObject empty string, it ends up trying to load "." before giving up

Change 3355297 on 2017/03/20 by Dan.Oconnor

	Fix incorrect flag settings from fast skeleton path.. this is part of the fix for UE-43083

Change 3355373 on 2017/03/20 by Michael.Noland

	PR #3222: Allow Blueprint Variables to be Readonly (Contributed by FrostByteGER)

	#jira UE-41640

Change 3355417 on 2017/03/20 by Ben.Zeigler

	Fix formatting bug where I forgot some braces

Change 3355462 on 2017/03/20 by Aaron.McLeran

	UE-43046 Property type changed with no possible conversion

	Resaved asset in question

Change 3355629 on 2017/03/20 by Dan.Oconnor

	Don't warn the user when their return node that has no pins (other than an exec pin). These return nodes cannot be deleted and connecting them does nothing. Prior to recent changes the warning never fired because the return node would be pruned and not validated.

Change 3355631 on 2017/03/20 by Dan.Oconnor

	Fix compilation results spam in compilation manager. Scoped compiler events (e.g. BP_SCOPED_COMPILER_EVENT_STAT(EKismetCompilerStats_CompileTime);) will flush the results log if no 'event' has been started. Timing data collected via this mechanism will not be useful (can only measure entire call to ::Flush in compilation manager)

Change 3356127 on 2017/03/21 by Richard.Hinckley

	#jira UEDOC-4711
	Updated an invalid/old URL in a comment to a valid/current URL.

Change 3356193 on 2017/03/21 by Marc.Audy

	Temporarily remove editor only properties in FCameraFocusSettings until we correctly no longer create pins for struct properties that are not exposed to blueprints
	#jira UE-43420

Change 3356222 on 2017/03/21 by Marc.Audy

	Expose new attenuation settings to blueprints to resolve cook warnings.

Change 3356286 on 2017/03/21 by Richard.Hinckley

	#jira UEDOC-4711
	Selected a different URL for the update.

Change 3356339 on 2017/03/21 by Marc.Audy

	Delete unconnected return nodes to fix fortnite cook warnings

Change 3356827 on 2017/03/21 by Ben.Zeigler

	Explicitly disable copy operations for streamable manager objects. This may be causing some obscure crashes like WEX-5182 but I am not sure how the copy constructor would be getting called. Either way it's unsafe
	Put in protection against passing in duplicate items to RequestAsyncLoad, which is another possible cause of internal data corruption
	Add some more ensures to track down possible issues with handle corruption

Change 3356920 on 2017/03/21 by Ben.Zeigler

	Fix ensure just checked in to not go off when handles are halfway through being cancelled

Change 3358152 on 2017/03/22 by Phillip.Kavan

	#jira UE-43102 - Fix an occasional crash on load in nativized EDL-enabled builds with non-nativized child BPs.

	Change summary:
	- Modified AActor::PostLoadSubobjects() to skip the CheckAndApplyComponentTemplateOverrides() call in the CDO case; at that point the ICH may not be fully loaded, but we don't require the non-nativized child BP's CDO to be fixed up anyway.

[CL 3358685 by Marc Audy in Main branch]
2017-03-22 12:57:30 -04:00

3669 lines
128 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 "Components/SynthComponent.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");
}
else if (BaseClass == USoundEffectSourcePreset::StaticClass())
{
return TEXT("SoundEffectSourceClass.h.template");
}
else if (BaseClass == USoundEffectSubmixPreset::StaticClass())
{
return TEXT("SoundEffectSubmixClass.h.template");
}
else if (BaseClass == USynthComponent::StaticClass())
{
return TEXT("SynthComponentClass.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");
}
else if (BaseClass == USynthComponent::StaticClass())
{
return TEXT("SynthComponentClass.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->GetNameStringByValue(InProjectInfo.TargetedHardware) : FString()));
Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EGraphicsPreset"), true);
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("GraphicsPreset"), Enum ? Enum->GetNameStringByValue(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 ( const FTemplateReplacement& Replacement : TemplateDefs->ReplacementsInFiles )
{
ReplacementsInFilesExtensions.Append(Replacement.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 ( const FString& SrcFilename : FilesToCopy )
{
// 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
if ( TemplateDefs->FilesToIgnore.Contains(SrcFileSubpath) )
{
// This file was marked as "ignored"
continue;
}
// Skip any folders that were configured to be ignored
if ( const FString* IgnoredFolder = TemplateDefs->FoldersToIgnore.FindByPredicate([&SrcFileSubpath](const FString& Ignore){ return SrcFileSubpath.StartsWith(Ignore + TEXT("/")); }) )
{
// This folder was marked as "ignored"
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Skipping as it is in an ignored folder '%s'"), *SrcFilename, **IgnoredFolder);
continue;
}
// Retarget any folders that were chosen to be renamed by choosing a new destination subpath now
FString DestFileSubpathWithoutFilename = FPaths::GetPath(SrcFileSubpath) + TEXT("/");
for ( const FTemplateFolderRename& FolderRename : TemplateDefs->FolderRenames )
{
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 ( const FTemplateReplacement& Replacement : TemplateDefs->FilenameReplacements )
{
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 ( const FString& FileToFix : FilesThatNeedContentsReplaced )
{
InnerSlowTask.EnterProgressFrame();
bool bSuccessfullyProcessed = false;
FString FileContents;
if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) )
{
for ( const FTemplateReplacement& Replacement : TemplateDefs->ReplacementsInFiles )
{
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);
ConfigValuesToSet.Emplace(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), FGuid::NewGuid().ToString(), /*InShouldReplaceExistingValue=*/true);
// Add all classname fixups
for ( const TPair<FString, FString>& Rename : ClassRenames )
{
const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *Rename.Key, *Rename.Value);
ConfigValuesToSet.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false);
}
// Fix all specified config values
for ( const FTemplateConfigValue& ConfigValue : ConfigValuesToSet )
{
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 ( const FString& LineIn : FileLines )
{
FString Line = LineIn;
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 ( &LineIn != &FileLines.Last() )
{
// 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 ( FModuleDescriptor& ModuleInfo : Project.Modules )
{
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