You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#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 Change3303824on 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 Change3311855on 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 Change3347562on 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 Change3352065on 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 Change3352967on 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. Change3355631on 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]
3669 lines
128 KiB
C++
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
|