You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#lockdown Nick.Penwarden #rb none ========================== MAJOR FEATURES + CHANGES ========================== Change 2948319 on 2016/04/19 by Nick.Shin update zlib to v1.2.8 part 1 of 4 - doing this in stages for tracking purposes #jira UEPLAT-1246 - Update libWebsockets #jira UEPLAT-1221 - update websocket library Change 2948322 on 2016/04/19 by Nick.Shin update libwebsockets to v1.7.4 part 4 of 4 - doing this in stages for tracking purposes #jira UEPLAT-1246 - Update libWebsockets #jira UEPLAT-1221 - update websocket library #jira UEPLAT-1204 - Rebuild libwebsockets with SSL Change 2948661 on 2016/04/19 by Nick.Shin keep using old zlibs until they are recompiled with the newer version Change 2948737 on 2016/04/19 by Nick.Shin build warning fix Change 2949334 on 2016/04/20 by Nick.Shin fix library path for some reason, NetworkFileSystem and HttpNetworkReplayStreaming on Mac platform needs full path - even though lib path was set... Change 2951556 on 2016/04/21 by Nick.Shin static libs double checked #jira UE-29674 - Editor fails to open in Dev-Platform Change 2951559 on 2016/04/21 by Nick.Shin static libs double checked forgot these files - they were in another changelist #jira UE-29674 - Editor fails to open in Dev-Platform Change 2952411 on 2016/04/22 by Nick.Shin add win32 build targets for zlib openssl libcurl libwebsockets part 1 of 2: these are the C# build scripts Change 2970016 on 2016/05/07 by Nick.Shin undo all of the following upgrades: - zlib - openssl - libcurl - libwebsockets and reset webrtc #jira UE-30298 - Fortnite and Orion crash on login Change 3118163 on 2016/09/08 by Josh.Adams perm test 2, not a useful file at all Change 3121142 on 2016/09/12 by Daniel.Lamb Attempt to fix deterministic cooking issue for particlelodlevel. Ensure the spawn module has had postload called on it before using. #test Paragon cook Change 3121150 on 2016/09/12 by Daniel.Lamb Added warning logs to help track down issue UE-33453. Change 3121201 on 2016/09/12 by Keith.Judge Xbox One - Replicate CL 3114357 from 4.13 branch. ESRAM clear on create fix. Change 3121302 on 2016/09/12 by Joe.Graf Fixed up the IMPLEMENT_MODULE macro usage to avoid the link errors Change 3121379 on 2016/09/12 by Dmitry.Rekman Linux: only link libraries that export needed symbols (UE-35720). - Fixes very long startup times of modular builds. - Includes PR #2778 by slonopotamus. #jira UE-35720 Change 3121383 on 2016/09/12 by Dmitry.Rekman Linux: added some missing _API declarations on symbols used externally. - Compiling editor with -fvisibility=hidden works after this fix (although running still doesn't). Change 3121456 on 2016/09/12 by Daniel.Lamb Attempt to fix deterministic cooking issue for particlelodlevel. Ensure the spawn module has had postload called on it before using. #test Paragon cook Change 3122939 on 2016/09/13 by Luke.Thatcher [PLATFORM] [PS4] [!] Skip orbismemdmp files in the PS4 crash handler web service. - Writing these files to disk causes orbis-tm.exe to take a file lock on them, which means we can't move the crash directory to the landing zone. Change 3123040 on 2016/09/13 by Brent.Pease + Fix VS compile error by removing ENGINE_API from virtual method decls since ENGINE_API is defined for the entire class now. Change 3123664 on 2016/09/13 by Nick.Shin this was originally checked into: release 4.13.1 bringing here to dev-platform -- original submit comments -- first, safari has a problem with firing off "window resized" events - causing an infinite loop of the window "resizing" next, retina has "bigger" size calculations going off -- so y-delta checks greater than 2 are done to prevent resize event firing off in an infinite loop jira UE-35363 - Huge game window when launching onto Safari 9.1.2 Change 3125282 on 2016/09/14 by Michael.Trepka Fixed iOS and tvOS code indexing in Xcode project Change 3126812 on 2016/09/15 by Josh.Adams Merged Wolf support into Dev-Platform (hidden from almost all people still). Non-Wolf-specific changes: - Added Parse function to JsonObject.cs to be able to parse a string - Replaced some hacky post-reflection-capture functions with RHISubmitCommandsAndFlushGPU() - Split PLATFORM_HAS_BSD_SOCKET_FEATURE_GETADDRINFO off from PLATFORM_HAS_BSD_SOCKET_FEATURE_GETHOSTNAME - Converted the PS4MallocCrash class into a generic one (that Wolf is now also using) - Added AddGenericToInQueueOnlineThread(), useful running a delegate on Online thread instead of game thread - Refactored the GL shader compiler to allow Wolf to modify behavior without a lot of if WOLF checks everywhere - Added ability in the cross compiler to convert the global uniform arrays into named uniform buffer objects - Added ability for GL shader compiler to output original resources names ("VertColor" instead of "u_v[3]" or whatever) - Added "FORCELODGROUP" console command that will apply a StaticMesh LODGroup to selected meshes in the editor. This can batch-Simplygonify all meshes in a level. Should maybe become an editor tool. - Added ability for arrays of structs to specify a property to be the key. So, with LODGroups, the Name key inside the struct can be the unique key, so when you have multiple .ini files in the hierarchy overriding the same LODGroup by name, it will repalce the first with the second, instead of adding two entries with the same name. Set by @ArrayName=KeyPropertyName. Per Object Config sections need a little different handling, which uses * (see BaseDeviceProfiles.ini) - Added ability to change DeviceProfiles at runtime. Use "dp.override <name>". If you do it again to another one, it will reset the settings to what they were originally, before applying the second new DP. This is because the second DP may not set all settings the first one did, but we want to undo the first settings that the second doesn't contain. - Added FRHICommandListImmediate::IsStalled() - returns true while FRHICommandListImmediate::StallRHIThread is happening - Changed runtime GetFeatureLevelMaxTextureSamplers() calls to the new GetMaxTextureSamplers() which can now be handled by the platform. Renamed GetFeatureLevelMaxTextureSamplers to GetExpectedFeatureLevelMaxTextureSamplers() (only used by the shader editor) to guess at what maybe the samplers count will be - but it's not guaranteed correct. - Renamed a UT copy of a global function to not linker-conflict - Changed the OOMBackupMemoryPool to allow each platform to set how much memory to allocate. See FPlatformMemory::GetBackMemoryPoolSize(). Defaults to 0, which was the previous behavior with the now removed FPlatformMemory::SupportBackupMemoryPool(), which was only true in Windows and PS4. - Added an OOM delegate so other systems can get a callback after OOM occurs (after deleting the backup memory pool if it exists) - Changed SetQualityLevels() (in Scalability.cpp) to no longer change the SetBy priority when setting CVars, and now keeps the SetBy the same as it was. Helps with conflicts between game settings and device profiles. See SetWithCurrentPriority() - Added GetRenderingThreadPriority to FPlatformAffinity to allow a platform override priority. Not sure about this one, so may remove it, or maybe add more priorities for all the threads? - Added a new file into the ini hierarchy to begin fixing the Engine/Base -> Project/Default -> Engine/Platform -> Project/Platform mess. We now have Engine/Base -> Engine/BasePlatform -> Project/Default -> Engine/Platform -> Project/Platform. However, Engine/Platform will soonm be deprecated as we move things over to Engine/BasePlatform, that are safe to move. Change 3126842 on 2016/09/15 by Michael.Trepka Make SAssertPicker's search box the default widget to focus on activate so that it doesn't get deactivated on Mac, where we get the window activation event in a tick after SAssertPicker creation. Change 3126956 on 2016/09/15 by Michael.Trepka Added support for compiling Vulkan shaders for Android on Mac Change 3127206 on 2016/09/15 by Michael.Trepka PR #2604: Remove some warnings. (Contributed by reapazor) Change 3127324 on 2016/09/15 by Michael.Trepka Allow third party dylibs on Mac to be loaded from plugin subfolders Change 3127924 on 2016/09/16 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) Change 3128369 on 2016/09/16 by Nick.Shin zlib 1.2.8 headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128377 on 2016/09/16 by Nick.Shin openssl 1_0_2h headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128383 on 2016/09/16 by Nick.Shin libcurl 7_48_0 headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128384 on 2016/09/16 by Nick.Shin libwebsockets 1.7.4 headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128464 on 2016/09/16 by Nick.Shin webRTC rev.12643 NOTE: VS2015 - only Win64 is available - Win32 versions is crashing (e.g. EpicGamesLauncher) at the moment NOTE: VS2013 - not tested (i'm working on getting a VS2013 pro license) - so not checking in with this changelist - also, VS2013 is no longer supported by webRTC build scripts, so it will be old anyways FUTURE NOTE: - will continue to try to get VS2015 Win32 functional - and am working on trying to get VS2013 tested headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128500 on 2016/09/16 by Nick.Shin zlib 1.2.8 - OSX headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128504 on 2016/09/16 by Nick.Shin openssl 1_0_2h - OSX headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change3128506on 2016/09/16 by Nick.Shin libcurl 7_48_0 - OSX headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128508 on 2016/09/16 by Nick.Shin libwebsockets 1.7.4 - OSX headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128513 on 2016/09/16 by Nick.Shin webRTC rev.12643 - OSX headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128602 on 2016/09/16 by Nick.Shin webRTC rev.9862 - Win64 VS2013 NOTE: - not tested (i'm working on getting a VS2013 pro license) - checking in for testing purposes WARNING: - VS2013 is no longer supported by webRTC latest headers and lib updates part of [ zlib openssl libcurl libwebsockets webrtc ] updates Change 3128605 on 2016/09/16 by Nick.Shin re-enabling updated ThirdParySoftware libs: - zlib (v.1.2.8) - openssl (1.0.2h) - libcurl (7_48_0) - libwebsocket (v.1.7.4) - webRTC (rev.12643) to the codereviewers, in my attempt to ensure the older libs are still used for console, mobile and linux -- please refer to this checkin if i broke the build... Change 3128651 on 2016/09/16 by Nick.Shin fix Win32 build error from CL: #3128605 Change 3128704 on 2016/09/16 by Nick.Shin fix Win32 build error from CL: #3128605 - this time actually compiling it... Change 3128825 on 2016/09/16 by Dmitry.Rekman Linux: proper fix for too slow startup times (UE-35967). - Pull request #2793 by slonopotamus. - Now without stripping dependencies on libraries specified before. - Contains a work around for ld bug <2.25. Change 3128972 on 2016/09/16 by Nick.Shin fix to local build error. Change 3129283 on 2016/09/16 by Brent.Pease + Add Android local notification support based on existing system used for iOS + Initial API has been added for cancelling local notifications but the actual platform implementation will be done in the next release Change 3129494 on 2016/09/17 by Nick.Shin fix CIS build errors Change 3129503 on 2016/09/17 by Dmitry.Rekman Fix Linux build (case sensitivity issue). Change 3129514 on 2016/09/17 by Nick.Shin fix CIS build errors for consoles - missing zlib include path special thanks to Dmitry.Rekman for pointing me in the right direction Change 3129647 on 2016/09/17 by Dmitry.Rekman Linux: fix non-unity build. Change 3131043 on 2016/09/19 by Nick.Shin archiving build instructions/steps when building: - zlib (v.1.2.8) win: #3128369 osx: #3128500 - openssl (1.0.2h) win: #3128377 osx: #3128504 - libcurl (7_48_0) win: #3128383 osx: #3128506 - libwebsocket (v.1.7.4) win: #3128384 osx: #3128508 - webRTC win: #3128464 (rev.12643 for vs2015) + 3128602 (rev:9862 for vs2013) -- NOTE: win32 is WiP osx: #3128513 Change 3132801 on 2016/09/20 by Dmitry.Rekman Linux: support specifying default OpenGL version via configs (UE-34777). - The first targeted RHI is going to be used. Change 3132905 on 2016/09/20 by Josh.Adams - Fixed up some paths with the WolfPlat rename Change 3133148 on 2016/09/20 by Josh.Adams - Only show UT EULA if PLATFORM_DESKTOP Change 3133152 on 2016/09/20 by Josh.Adams - Beginning support for applets. Disabled unless you have a special SDK with applet support. Change 3133169 on 2016/09/20 by Josh.Adams - Fixed issue with Wolf access but no SDK installed Change 3133344 on 2016/09/20 by Daniel.Lamb Fixed issue with Iterative cooking not detecting changes to ini files which are loaded using LoadLocalFile. Added new flag to limit number of concurrent shader compiles. #test Cook QAGame, Cook Paragon Change 3133345 on 2016/09/20 by Daniel.Lamb FRedirectCollector collects string asset references all the time when running the editor. #test Cook paragon cook QAGame. Change 3133852 on 2016/09/21 by Luke.Thatcher [PLATFORM] [PS4] [^] Performing merge between 3.508.201 LCUE files in CarefullyRedist and Dev-Platform to populate integration history. No files have actually changed in this CL, only Perforce metadata is updated. Change 3133875 on 2016/09/21 by Luke.Thatcher [PLATFORM] [PS4] [^] Performing merge between 3.508.201 LCUE files in CarefullyRedist and Dev-Platform to populate integration history. No files have actually changed in this CL, only Perforce metadata is updated. (Attempt 2) Change 3134403 on 2016/09/21 by Jonathan.Fitzpatrick Per PS4 documentation, app_type requires the alternate spelling of 'upgradeable', 'upgradable'. Change 3134544 on 2016/09/21 by Josh.Adams - Reduced UT textures for Wolf Change 3134915 on 2016/09/21 by Jonathan.Fitzpatrick FPS4Time::SystemTime now calculates the local machine time, instead of UTC. #jira UE-35170 Change 3135036 on 2016/09/21 by Michael.Trepka Quit the UE4EditorServices app when quitting the Launcher if it was the launcher that spawned the services process Change 3135142 on 2016/09/21 by Jonathan.Fitzpatrick GetBackMemoryPoolSize returned bool on PS4 by accident, should be uint32 Change 3135292 on 2016/09/21 by Jeff.Campeau Change include order to favor the XDK edition specific headers where available. Change 3136414 on 2016/09/22 by Josh.Adams - Fixed a checkf() that had the case reversed #jira ue-36311 Change 3137082 on 2016/09/22 by Dmitry.Rekman Added support for Linux installed builds to 4.14 Change 3137220 on 2016/09/22 by Dmitry.Rekman Linux: do not rebuild hlslcc on each setup. - Now that hlslcc is set to use bundled libc++ there should be no STL binary compatibility conflicts between the engine and hlslcc binary. Change 3137227 on 2016/09/22 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) Change 3137259 on 2016/09/22 by Dmitry.Rekman Linux installed build: fix CIS (missed one .csproj) Change 3137290 on 2016/09/22 by Dmitry.Rekman Linux installed builds: fix for the resulting directory. Change 3137291 on 2016/09/22 by Chris.Babcock Restore texture filtering mode properly when movie played on Android #jira UE-36342 #ue4 #android Change 3137376 on 2016/09/22 by Dmitry.Rekman Linux: re-enabled crash handler stack smash protection. - Race condition in FRunnableThreadPThread has been previously fixed. Change 3138498 on 2016/09/23 by Dmitry.Rekman Linux: add missed package for installed builds. - mono-devel package for resgen2. Change 3138523 on 2016/09/23 by Dmitry.Rekman Linux: Update hlslcc now that we're not rebuilding it each time. Change 3138658 on 2016/09/23 by Josh.Adams - Moved UT's Social Plugin into NotForLicensees Change 3139042 on 2016/09/23 by Dmitry.Rekman Linux: more robust check of installed packages. - Also added mono-devel to the list of packages installed on 14.04. Change 3139674 on 2016/09/26 by Dmitry.Rekman Fix crash when editing widget blueprints (UE-35185). - Caused by name collision due to copy/pasted code; aliased classes diverged and this resulted in all kinds of weird memory stomping. - Renamed the class and also applied the same workaround (removing static) to prevent likely crashes on exit as happened with the original class (see UE-30795). Change 3140203 on 2016/09/26 by Josh.Adams - Wolf Fix for SHIPPING Change 3140206 on 2016/09/26 by Josh.Adams - NEX work, still in progress Change 3140276 on 2016/09/26 by Josh.Adams - Fixed Wolf compile error Change 3140485 on 2016/09/26 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) Change 3140570 on 2016/09/26 by Dmitry.Rekman SDL2: Delete obsolete files. - We now have local changes to SDL2, so this tarball is no longer accurate and just takes unnecessary space. Change 3140577 on 2016/09/26 by Dmitry.Rekman Fix CudaTest monolithic build. - Not the best fix, the better fix is to build against bundled libc++. Change3141184on 2016/09/27 by Keith.Judge Add FXboxOneApplication::GetXboxOneApplication to fix a save/load game assert. #jira UE-35973 Change 3141623 on 2016/09/27 by Chris.Babcock Support hiding virtual keyboard on Android #jira UE-34201 #ue4 #android Change 3141887 on 2016/09/27 by Joe.Graf Added support for additional plugin directories that are specified by the .uproject file New plugin wizard adds to the additional plugin directories if the user specifies a directory outside of Engine/Plugins or Game/Plugins Change 3141916 on 2016/09/27 by Josh.Adams - Worked around compile issues (at least with Wolf UT). This is well documented in a Jira (UE-29925) Change 3141926 on 2016/09/27 by Josh.Adams - Support for skipping Wolf user selector (-nologinui) Change 3141938 on 2016/09/27 by Chris.Babcock Allow Android media player to seek past 999ms (contributed by rcywongaa) #jira UE-36453 #PR #2797 #ue4 #android Change 3142207 on 2016/09/27 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) Change 3142219 on 2016/09/27 by Josh.Adams - Wolf PhysX 3.4 libs and includes Change 3142220 on 2016/09/27 by Josh.Adams - File that had to be fixed up after main merge (missed adding it to the huge integrate CL) Change 3142314 on 2016/09/27 by Chase.McAllister #jira UE-35011 fixes to some assets to remove redundancies/output log spam Change 3142510 on 2016/09/27 by Daniel.Lamb Fixed up resave lightmaps commandlet so that world transforms don't get applied twice. #jira UE-35942 Change 3142650 on 2016/09/27 by Chris.Babcock Android support for Linux by yaakuro - requires CodeWorks for Android Linux installed and OpenJDK 1.8 - need to set Android SDK paths manually in Project Settings #jira UE-32752 #jira UE-32753 #PR #2564 #PR #2565 #ue4 #android #linux Change3142802on 2016/09/27 by Dmitry.Rekman Upgrade to SDL 2.0.5-ish (still technically 2.0.4). - Upstream revision 10374:dccf51aee79b. - Merged all our changes hopefully. Change 3143075 on 2016/09/28 by Luke.Thatcher [RENDERING] [~] Add check to FBatchedElements::AddSprite to catch null textures. If the texture is null here, we will crash later in the RHI. At least now we'll get the callstack of the code adding the null textured sprite, since I don't have a repro. #jira UE-33077 Change 3143219 on 2016/09/28 by Daniel.Lamb Added new is compiling function which tells you if it's really compiling instead of lying. If def out additional logging for debugging shader compilation issue for 4.14 release. Change 3143428 on 2016/09/28 by Luke.Thatcher [PLATFORM] [PS4] [+] Use PS4 SDK 4.008.061 Change 3143488 on 2016/09/28 by Daniel.Lamb Changed defaults for skip cooking editor content to true. Change 3143526 on 2016/09/28 by Daniel.Lamb Increased the concurrent shader compile limit while in the cooker. #test Cook paragon Change 3143874 on 2016/09/28 by Chris.Babcock Read Android environment variables from .bashrc on Linux #jira UE-36565 #ue4 #android #linux Change 3143911 on 2016/09/28 by Dmitry.Rekman Fix SDL EGL API binding (UE-18979). - Contains PR #1398 by x414e54. - Also fixes offscreen backend that needed to provide a global mouse state after the SDL upgrade. Change 3143929 on 2016/09/28 by Daniel.Lamb Removed some more temporary logging. #test Cook paragon Change 3143959 on 2016/09/28 by Jeff.Campeau Media Player for Xbox One Change 3143997 on 2016/09/28 by Dmitry.Rekman Linux: faster linking in Debug. - Do not apply --as-needed to Debug build since taking a hit of several tens of seconds on startup is better than linking for ~4 more minutes when iterating. Change 3144004 on 2016/09/28 by Dmitry.Rekman Linux: make SCW dump core on crash in debug builds. - If the editor (not SCW itself) is built in Debug, make SCW dump cores if they ever crash. This makes it debug easier (at the risk of running of disk space). Change 3144007 on 2016/09/28 by Dmitry.Rekman Linux: Allow equals character in command line parameter value (UE-26406). - PR #2019 by bozzaro. - Allows passing parameters like -Switch=Key=Value. Change 3144042 on 2016/09/28 by Jeff.Campeau Add tag for DX12 support being experimental in target settings. #jira UE-36150 Change 3144068 on 2016/09/28 by Dmitry.Rekman Linux: enable using xgConsole in UAT (UE-28096). - PR #2144 by bozzaro. - Picks correct xgConsole binary. - Allegedly fixes crash in CombineXGEItemFile on mono. Change 3144120 on 2016/09/28 by Michael.Trepka Copying //Tasks/UE4/Dev-HighDPI/... to //UE4/Dev-Platform/... Change 3144172 on 2016/09/28 by Chris.Babcock Add libpng 1.5.27 for Android #jira UE-36573 #ue4 #android Change 3144318 on 2016/09/28 by Chris.Babcock Correct logic for checking .bashrc on Linux #ue4 #android Change 3144331 on 2016/09/28 by Dmitry.Rekman Linux: repair ARM server builds. - Also: print info about C++ library being used and allow the override via environment variable UE4_LINUX_USE_LIBCXX (either 0 or 1). Change 3144354 on 2016/09/28 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) this is intermediate, not fully working Change 3144368 on 2016/09/28 by Josh.Adams - Moved the new Social files into NFL Change 3144395 on 2016/09/28 by Chris.Babcock Add missing functions for AndroidWebBrowserWindow #ue4 #android Change 3144417 on 2016/09/28 by Josh.Adams - Probable fix for FWebBrowserWindow missing virtuals Change 3144438 on 2016/09/28 by Jeff.Campeau XDK updated to 160802 Change 3144569 on 2016/09/29 by Dmitry.Rekman Linux: allow a selectable clock source (UE-36564). - The engine will now select the best performing clock on start instead of hard-coding CLOCK_REALTIME. This will happen as part of global initialization before main() to prevent clock skew. - Also fixes a problem of the engine not being able to start on Windows 10 since previously hard-coded clock id was not supported there. #tests Compiled and ran a few targets (including non-monolithic). Tried bogus clock sources. Haven't actually tried on Win10 (don't have a machine atm). Change 3145108 on 2016/09/29 by Joe.Graf Fixed cases where path relative external plugin paths would generate the wrong path when running Unreal Header Tool (and probably other tools) Change 3145245 on 2016/09/29 by Joe.Graf #wolf Checking in removal of plugin use on Win64 per Josh's request Change 3145514 on 2016/09/29 by Will.Fissler Updated Mac Info.plist files to disable high DPI on macOS 10.12 Change 3145538 on 2016/09/29 by Josh.Adams - Worked around a physics task graph issue with using the new lock free stuff on Wolf, joining PS4 and XboxOne. Wolf was crashing on some boots. Change 3145540 on 2016/09/29 by Josh.Adams - Fix for checking some Wolf dev tool installation existence - Fix for various Wolf build issues - Fix for Wolf devices not showing up in Launch on Change 3145542 on 2016/09/29 by Josh.Adams - Pulled over Wolf changes from Wolf branch into Dev-Platform Change 3145572 on 2016/09/29 by Josh.Adams - Cleaned up Wolf SDK error logs which really messed up GenProjectFiles for some class of people. #jira UE-36591 Change 3145769 on 2016/09/29 by Chris.Babcock Remove duplicate platforms from deploy list in UFE #jira UE-36636 #ue4 Change 3146061 on 2016/09/29 by Chris.Babcock Linux: be less spammy in log when launching external procs #jira UE-36638 #ue4 #linux Change 3146208 on 2016/09/29 by Dmitry.Rekman Linux: fix PhysX crash (UE-36613). - PX_RESTRICT was unwarrantedly applied to memMove, allowing clang to replace the memmove() call to memcpy() at -O2 and above. - This caused PxArray::remove() to duplicate the elements of its array (in POD case) and this opened doors to all kinds of fun. #jira UE-36613 Change 3146476 on 2016/09/30 by Josh.Adams - Moved a UBT log that could pollute QA logs with Wolf secrets to Verbose Change 3146554 on 2016/09/30 by Josh.Adams - Removed another wolf secret log Change 3146626 on 2016/09/30 by Josh.Adams Merging //UE4/Dev-Main to Dev-Platform (//UE4/Dev-Platform) Change 3146712 on 2016/09/30 by Josh.Adams - Fixed case for building Android on Linux #jira #UE-36652 Change 3146844 on 2016/09/30 by Josh.Adams - Removed ES2 shader compiling from TVOS, and force Metal compiling #jira UE-36306 Change 3146865 on 2016/09/30 by Daniel.Lamb Removed temp logging for materials #test Launch on paragon Change 3146874 on 2016/09/30 by Dmitry.Rekman Linux: add rpath for libTextureConverter.so (UE-36620). Change 3147030 on 2016/09/30 by Josh.Adams - Version check workaround for IOS9.3/TVOS9.2 defining __IPHONE_10_0 which breaks our IOS10 code checks #jira UE-36623 Change 3147151 on 2016/09/30 by Josh.Adams - Fixed zlib.build.cs for XboxOne, which came in from another branch without an include path, yet somehow main is compiling? Change 3147621 on 2016/09/30 by Michael.Trepka Fix for setting up RPATHs for third party dylibs for packaged code-based games on Mac Change 3147712 on 2016/09/30 by Josh.Adams - Fixed metal crash StrategyGame crash. Recent code was checking IsES2Platform for HDR decoding in scene capture, and Metal hasn't been IsES2 since may. Changed to IsMobilePlatform. #jira UE-36225 Change 3147725 on 2016/09/30 by Josh.Adams - Fixed yet another Wolf log for people with Wolf access but no SDK [CL 3147801 by Josh Adams in Main branch]
3666 lines
128 KiB
C++
3666 lines
128 KiB
C++
// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "GameProjectGenerationPrivatePCH.h"
|
|
#include "FeaturedClasses.inl"
|
|
|
|
#include "UnrealEdMisc.h"
|
|
#include "ISourceControlModule.h"
|
|
#include "MainFrame.h"
|
|
#include "DefaultTemplateProjectDefs.h"
|
|
|
|
#include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h"
|
|
#include "EngineAnalytics.h"
|
|
#include "EngineBuildSettings.h"
|
|
|
|
#include "DesktopPlatformModule.h"
|
|
#include "TargetPlatform.h"
|
|
|
|
#include "SlateIconFinder.h"
|
|
#include "Editor/UnrealEd/Public/SourceCodeNavigation.h"
|
|
|
|
#include "UProjectInfo.h"
|
|
#include "DesktopPlatformModule.h"
|
|
#include "SNotificationList.h"
|
|
#include "NotificationManager.h"
|
|
#include "GameFramework/GameModeBase.h"
|
|
#include "HotReloadInterface.h"
|
|
#include "SVerbChoiceDialog.h"
|
|
#include "SourceCodeNavigation.h"
|
|
#include "FeaturePackContentSource.h"
|
|
|
|
#include "SOutputLogDialog.h"
|
|
|
|
#include "Sound/SoundEffectSubmix.h"
|
|
#include "Sound/SoundEffectSource.h"
|
|
|
|
#include "PlatformInfo.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "GameProjectUtils"
|
|
|
|
#define MAX_PROJECT_PATH_BUFFER_SPACE 130 // Leave a reasonable buffer of additional characters to account for files created in the content directory during or after project generation
|
|
#define MAX_PROJECT_NAME_LENGTH 20 // Enforce a reasonable project name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH
|
|
static_assert(PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE > 0, "File system path shorter than project creation buffer space.");
|
|
|
|
#define MAX_CLASS_NAME_LENGTH 32 // Enforce a reasonable class name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH
|
|
|
|
TWeakPtr<SNotificationItem> GameProjectUtils::UpdateGameProjectNotification = NULL;
|
|
TWeakPtr<SNotificationItem> GameProjectUtils::WarningProjectNameNotification = NULL;
|
|
|
|
FString GameProjectUtils::DefaultFeaturePackExtension(TEXT(".upack"));
|
|
|
|
FText FNewClassInfo::GetClassName() const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
return BaseClass ? BaseClass->GetDisplayNameText() : FText::GetEmpty();
|
|
|
|
case EClassType::EmptyCpp:
|
|
return LOCTEXT("NoParentClass", "None");
|
|
|
|
case EClassType::SlateWidget:
|
|
return LOCTEXT("SlateWidgetParentClass", "Slate Widget");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return LOCTEXT("SlateWidgetStyleParentClass", "Slate Widget Style");
|
|
|
|
case EClassType::UInterface:
|
|
return LOCTEXT("UInterfaceParentClass", "Unreal Interface");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
FText FNewClassInfo::GetClassDescription(const bool bFullDescription/* = true*/) const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
{
|
|
if(BaseClass)
|
|
{
|
|
FString ClassDescription = BaseClass->GetToolTipText(/*bShortTooltip=*/!bFullDescription).ToString();
|
|
|
|
if(!bFullDescription)
|
|
{
|
|
int32 FullStopIndex = 0;
|
|
if(ClassDescription.FindChar('.', FullStopIndex))
|
|
{
|
|
// Only show the first sentence so as not to clutter up the UI with a detailed description of implementation details
|
|
ClassDescription = ClassDescription.Left(FullStopIndex + 1);
|
|
}
|
|
|
|
// Strip out any new-lines in the description
|
|
ClassDescription.ReplaceInline(TEXT("\n"), TEXT(" "));
|
|
}
|
|
|
|
return FText::FromString(ClassDescription);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EClassType::EmptyCpp:
|
|
return LOCTEXT("EmptyClassDescription", "An empty C++ class with a default constructor and destructor.");
|
|
|
|
case EClassType::SlateWidget:
|
|
return LOCTEXT("SlateWidgetClassDescription", "A custom Slate widget, deriving from SCompoundWidget.");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return LOCTEXT("SlateWidgetStyleClassDescription", "A custom Slate widget style, deriving from FSlateWidgetStyle, along with its associated UObject wrapper class.");
|
|
|
|
case EClassType::UInterface:
|
|
return LOCTEXT("UInterfaceClassDescription", "A UObject Interface class, to be implemented by other UObject-based classes.");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
const FSlateBrush* FNewClassInfo::GetClassIcon() const
|
|
{
|
|
// Safe to do even if BaseClass is null, since FindIconForClass will return the default icon
|
|
return FSlateIconFinder::FindIconBrushForClass(BaseClass);
|
|
}
|
|
|
|
FString FNewClassInfo::GetClassPrefixCPP() const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
return BaseClass ? BaseClass->GetPrefixCPP() : TEXT("");
|
|
|
|
case EClassType::EmptyCpp:
|
|
return TEXT("");
|
|
|
|
case EClassType::SlateWidget:
|
|
return TEXT("S");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return TEXT("F");
|
|
|
|
case EClassType::UInterface:
|
|
return TEXT("U");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return TEXT("");
|
|
}
|
|
|
|
FString FNewClassInfo::GetClassNameCPP() const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
return BaseClass ? BaseClass->GetName() : TEXT("");
|
|
|
|
case EClassType::EmptyCpp:
|
|
return TEXT("");
|
|
|
|
case EClassType::SlateWidget:
|
|
return TEXT("CompoundWidget");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return TEXT("SlateWidgetStyle");
|
|
|
|
case EClassType::UInterface:
|
|
return TEXT("Interface");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return TEXT("");
|
|
}
|
|
|
|
FString FNewClassInfo::GetCleanClassName(const FString& ClassName) const
|
|
{
|
|
FString CleanClassName = ClassName;
|
|
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::SlateWidgetStyle:
|
|
{
|
|
// Slate widget style classes always take the form FMyThingWidget, and UMyThingWidgetStyle
|
|
// if our class ends with either Widget or WidgetStyle, we need to strip those out to avoid silly looking duplicates
|
|
if(CleanClassName.EndsWith(TEXT("Style")))
|
|
{
|
|
CleanClassName = CleanClassName.LeftChop(5); // 5 for "Style"
|
|
}
|
|
if(CleanClassName.EndsWith(TEXT("Widget")))
|
|
{
|
|
CleanClassName = CleanClassName.LeftChop(6); // 6 for "Widget"
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return CleanClassName;
|
|
}
|
|
|
|
FString FNewClassInfo::GetFinalClassName(const FString& ClassName) const
|
|
{
|
|
const FString CleanClassName = GetCleanClassName(ClassName);
|
|
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::SlateWidgetStyle:
|
|
return FString::Printf(TEXT("%sWidgetStyle"), *CleanClassName);
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return CleanClassName;
|
|
}
|
|
|
|
bool FNewClassInfo::GetIncludePath(FString& OutIncludePath) const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
if(BaseClass && BaseClass->HasMetaData(TEXT("IncludePath")))
|
|
{
|
|
OutIncludePath = BaseClass->GetMetaData(TEXT("IncludePath"));
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case EClassType::SlateWidget:
|
|
OutIncludePath = "Widgets/SCompoundWidget.h";
|
|
return true;
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
OutIncludePath = "Styling/SlateWidgetStyle.h";
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FString FNewClassInfo::GetBaseClassHeaderFilename() const
|
|
{
|
|
FString IncludePath;
|
|
|
|
switch (ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
if (BaseClass)
|
|
{
|
|
FString ClassHeaderPath;
|
|
if (FSourceCodeNavigation::FindClassHeaderPath(BaseClass, ClassHeaderPath) && IFileManager::Get().FileSize(*ClassHeaderPath) != INDEX_NONE)
|
|
{
|
|
return ClassHeaderPath;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case EClassType::SlateWidget:
|
|
case EClassType::SlateWidgetStyle:
|
|
GetIncludePath(IncludePath);
|
|
return FPaths::EngineDir() / TEXT("Source") / TEXT("Runtime") / TEXT("SlateCore") / TEXT("Public") / IncludePath;
|
|
default:
|
|
return FString();
|
|
}
|
|
|
|
return FString();
|
|
}
|
|
|
|
FString FNewClassInfo::GetHeaderFilename(const FString& ClassName) const
|
|
{
|
|
const FString HeaderFilename = GetFinalClassName(ClassName) + TEXT(".h");
|
|
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::SlateWidget:
|
|
return TEXT("S") + HeaderFilename;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return HeaderFilename;
|
|
}
|
|
|
|
FString FNewClassInfo::GetSourceFilename(const FString& ClassName) const
|
|
{
|
|
const FString SourceFilename = GetFinalClassName(ClassName) + TEXT(".cpp");
|
|
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::SlateWidget:
|
|
return TEXT("S") + SourceFilename;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return SourceFilename;
|
|
}
|
|
|
|
FString FNewClassInfo::GetHeaderTemplateFilename() const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
{
|
|
if (BaseClass != nullptr)
|
|
{
|
|
if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass()))
|
|
{
|
|
return TEXT("ActorComponentClass.h.template");
|
|
}
|
|
else if (BaseClass == AActor::StaticClass())
|
|
{
|
|
return TEXT("ActorClass.h.template");
|
|
}
|
|
else if (BaseClass == APawn::StaticClass())
|
|
{
|
|
return TEXT("PawnClass.h.template");
|
|
}
|
|
else if (BaseClass == ACharacter::StaticClass())
|
|
{
|
|
return TEXT("CharacterClass.h.template");
|
|
}
|
|
|
|
// Only check audio-mixer module specific classes if audio mixer is loaded
|
|
if (FModuleManager::Get().IsModuleLoaded("AudioMixer"))
|
|
{
|
|
if (BaseClass == USoundEffectSource::StaticClass())
|
|
{
|
|
return TEXT("SoundEffectSourceClass.h.template");
|
|
}
|
|
else if (BaseClass == USoundEffectSubmix::StaticClass())
|
|
{
|
|
return TEXT("SoundEffectSubmixClass.h.template");
|
|
}
|
|
}
|
|
}
|
|
// Some other non-actor, non-component UObject class
|
|
return TEXT( "UObjectClass.h.template" );
|
|
}
|
|
|
|
case EClassType::EmptyCpp:
|
|
return TEXT("EmptyClass.h.template");
|
|
|
|
case EClassType::SlateWidget:
|
|
return TEXT("SlateWidget.h.template");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return TEXT("SlateWidgetStyle.h.template");
|
|
|
|
case EClassType::UInterface:
|
|
return TEXT("InterfaceClass.h.template");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return TEXT("");
|
|
}
|
|
|
|
FString FNewClassInfo::GetSourceTemplateFilename() const
|
|
{
|
|
switch(ClassType)
|
|
{
|
|
case EClassType::UObject:
|
|
if (BaseClass != nullptr)
|
|
{
|
|
if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass()))
|
|
{
|
|
return TEXT("ActorComponentClass.cpp.template");
|
|
}
|
|
else if (BaseClass == AActor::StaticClass())
|
|
{
|
|
return TEXT("ActorClass.cpp.template");
|
|
}
|
|
else if (BaseClass == APawn::StaticClass())
|
|
{
|
|
return TEXT("PawnClass.cpp.template");
|
|
}
|
|
else if (BaseClass == ACharacter::StaticClass())
|
|
{
|
|
return TEXT("CharacterClass.cpp.template");
|
|
}
|
|
else if (BaseClass == USoundEffectSubmix::StaticClass())
|
|
{
|
|
return TEXT("SoundEffectSubmixClass.cpp.template");
|
|
}
|
|
else if (BaseClass == USoundEffectSource::StaticClass())
|
|
{
|
|
return TEXT("SoundEffectSourceClass.cpp.template");
|
|
}
|
|
}
|
|
// Some other non-actor, non-component UObject class
|
|
return TEXT( "UObjectClass.cpp.template" );
|
|
|
|
case EClassType::EmptyCpp:
|
|
return TEXT("EmptyClass.cpp.template");
|
|
|
|
case EClassType::SlateWidget:
|
|
return TEXT("SlateWidget.cpp.template");
|
|
|
|
case EClassType::SlateWidgetStyle:
|
|
return TEXT("SlateWidgetStyle.cpp.template");
|
|
|
|
case EClassType::UInterface:
|
|
return TEXT("InterfaceClass.cpp.template");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return TEXT("");
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidProjectFileForCreation(const FString& ProjectFile, FText& OutFailReason)
|
|
{
|
|
const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile);
|
|
if ( FPaths::GetPath(ProjectFile).IsEmpty() )
|
|
{
|
|
OutFailReason = LOCTEXT( "NoProjectPath", "You must specify a path." );
|
|
return false;
|
|
}
|
|
|
|
if ( BaseProjectFile.IsEmpty() )
|
|
{
|
|
OutFailReason = LOCTEXT( "NoProjectName", "You must specify a project name." );
|
|
return false;
|
|
}
|
|
|
|
if ( BaseProjectFile.Contains(TEXT(" ")) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." );
|
|
return false;
|
|
}
|
|
|
|
if ( !FChar::IsAlpha(BaseProjectFile[0]) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." );
|
|
return false;
|
|
}
|
|
|
|
if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters." ), Args );
|
|
return false;
|
|
}
|
|
|
|
const int32 MaxProjectPathLength = PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE;
|
|
if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args );
|
|
return false;
|
|
}
|
|
|
|
if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args );
|
|
return false;
|
|
}
|
|
|
|
FString IllegalNameCharacters;
|
|
if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args );
|
|
return false;
|
|
}
|
|
|
|
if (NameContainsUnderscoreAndXB1Installed(BaseProjectFile))
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." );
|
|
return false;
|
|
}
|
|
|
|
if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( ProjectFileExists(ProjectFile) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectFileAlreadyExists", "This project file already exists." );
|
|
return false;
|
|
}
|
|
|
|
if ( FPaths::ConvertRelativePathToFull(FPaths::GetPath(ProjectFile)).StartsWith( FPaths::ConvertRelativePathToFull(FPaths::EngineDir())) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectFileCannotBeUnderEngineFolder", "Project cannot be saved under the Engine folder. Please choose a different directory." );
|
|
return false;
|
|
}
|
|
|
|
if ( AnyProjectFilesExistInFolder(FPaths::GetPath(ProjectFile)) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "AProjectFileAlreadyExistsAtLoction", "Another .{ProjectFileExtension} file already exists in the specified folder" ), Args );
|
|
return false;
|
|
}
|
|
|
|
// Don't allow any files within target directory so we can safely delete everything on failure
|
|
TArray<FString> ExistingFiles;
|
|
IFileManager::Get().FindFiles(ExistingFiles, *(FPaths::GetPath(ProjectFile) / TEXT("*")), true, true);
|
|
if (ExistingFiles.Num() > 0)
|
|
{
|
|
OutFailReason = LOCTEXT("ProjectFileCannotBeWithExistingFiles", "Project cannot be saved in a folder with existing files. Please choose a different directory/project name.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::OpenProject(const FString& ProjectFile, FText& OutFailReason)
|
|
{
|
|
if ( ProjectFile.IsEmpty() )
|
|
{
|
|
OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." );
|
|
return false;
|
|
}
|
|
|
|
const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile);
|
|
if ( BaseProjectFile.Contains(TEXT(" ")) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." );
|
|
return false;
|
|
}
|
|
|
|
if ( !FChar::IsAlpha(BaseProjectFile[0]) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." );
|
|
return false;
|
|
}
|
|
|
|
const int32 MaxProjectPathLength = PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE;
|
|
if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args );
|
|
return false;
|
|
}
|
|
|
|
if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args );
|
|
return false;
|
|
}
|
|
|
|
FString IllegalNameCharacters;
|
|
if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args );
|
|
return false;
|
|
}
|
|
|
|
if (NameContainsUnderscoreAndXB1Installed(BaseProjectFile))
|
|
{
|
|
OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." );
|
|
return false;
|
|
}
|
|
|
|
if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !ProjectFileExists(ProjectFile) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("ProjectFile"), FText::FromString( ProjectFile ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "ProjectFileDoesNotExist", "{ProjectFile} does not exist." ), Args );
|
|
return false;
|
|
}
|
|
|
|
FUnrealEdMisc::Get().SwitchProject(ProjectFile, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::OpenCodeIDE(const FString& ProjectFile, FText& OutFailReason)
|
|
{
|
|
if ( ProjectFile.IsEmpty() )
|
|
{
|
|
OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." );
|
|
return false;
|
|
}
|
|
|
|
// Check whether this project is a foreign project. Don't use the cached project dictionary; we may have just created a new project.
|
|
FString SolutionFolder;
|
|
FString SolutionFilenameWithoutExtension;
|
|
if( FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFile) )
|
|
{
|
|
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(ProjectFile));
|
|
SolutionFilenameWithoutExtension = FPaths::GetBaseFilename(ProjectFile);
|
|
}
|
|
else
|
|
{
|
|
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir());
|
|
SolutionFilenameWithoutExtension = TEXT("UE4");
|
|
}
|
|
|
|
// Get the solution filename
|
|
FString CodeSolutionFile;
|
|
#if PLATFORM_WINDOWS
|
|
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".sln");
|
|
#elif PLATFORM_MAC
|
|
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".xcworkspace");
|
|
#elif PLATFORM_LINUX
|
|
// FIXME: Should depend on PreferredAccessor setting
|
|
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".workspace");
|
|
#else
|
|
OutFailReason = LOCTEXT( "OpenCodeIDE_UnknownPlatform", "could not open the code editing IDE. The operating system is unknown." );
|
|
return false;
|
|
#endif
|
|
|
|
// Open the solution with the default application
|
|
const FString FullPath = FPaths::Combine(*SolutionFolder, *CodeSolutionFile);
|
|
#if PLATFORM_MAC
|
|
if ( IFileManager::Get().DirectoryExists(*FullPath) )
|
|
#else
|
|
if ( FPaths::FileExists(FullPath) )
|
|
#endif
|
|
{
|
|
FPlatformProcess::LaunchFileInDefaultExternalApplication( *FullPath );
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("Path"), FText::FromString( FullPath ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "OpenCodeIDE_MissingFile", "Could not edit the code editing IDE. {Path} could not be found." ), Args );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void GameProjectUtils::GetStarterContentFiles(TArray<FString>& OutFilenames)
|
|
{
|
|
FString const SrcFolder = FPaths::FeaturePackDir();
|
|
|
|
FString SearchPath = TEXT("*");
|
|
SearchPath += DefaultFeaturePackExtension;
|
|
IFileManager::Get().FindFilesRecursive(OutFilenames, *SrcFolder, *SearchPath, /*Files=*/true, /*Directories=*/false);
|
|
}
|
|
|
|
bool GameProjectUtils::CreateProject(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray<FString>* OutCreatedFiles)
|
|
{
|
|
if ( !IsValidProjectFileForCreation(InProjectInfo.ProjectFilename, OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FScopedSlowTask SlowTask(0, LOCTEXT( "CreatingProjectStatus", "Creating project..." ));
|
|
SlowTask.MakeDialog();
|
|
|
|
bool bProjectCreationSuccessful = false;
|
|
FString TemplateName;
|
|
if ( InProjectInfo.TemplateFile.IsEmpty() )
|
|
{
|
|
bProjectCreationSuccessful = GenerateProjectFromScratch(InProjectInfo, OutFailReason, OutFailLog);
|
|
TemplateName = InProjectInfo.bShouldGenerateCode ? TEXT("Basic Code") : TEXT("Blank");
|
|
}
|
|
else
|
|
{
|
|
bProjectCreationSuccessful = CreateProjectFromTemplate(InProjectInfo, OutFailReason, OutFailLog, OutCreatedFiles);
|
|
TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
|
|
}
|
|
|
|
if (!bProjectCreationSuccessful && CleanupIsEnabled())
|
|
{
|
|
// Delete the new project folder
|
|
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
|
|
IFileManager::Get().DeleteDirectory(*NewProjectFolder, /*RequireExists=*/false, /*Tree=*/true);
|
|
if( OutCreatedFiles != nullptr )
|
|
{
|
|
OutCreatedFiles->Empty();
|
|
}
|
|
}
|
|
|
|
if( FEngineAnalytics::IsAvailable() )
|
|
{
|
|
TArray<FAnalyticsEventAttribute> EventAttributes;
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Template"), TemplateName));
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ProjectType"), InProjectInfo.bShouldGenerateCode ? TEXT("C++ Code") : TEXT("Content Only")));
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), bProjectCreationSuccessful ? TEXT("Successful") : TEXT("Failed")));
|
|
|
|
UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EHardwareClass"), true);
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HardwareClass"), Enum ? Enum->GetEnumName(InProjectInfo.TargetedHardware) : FString()));
|
|
Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EGraphicsPreset"), true);
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("GraphicsPreset"), Enum ? Enum->GetEnumName(InProjectInfo.DefaultGraphicsPerformance) : FString()));
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("StarterContent"), InProjectInfo.bCopyStarterContent ? TEXT("Yes") : TEXT("No")));
|
|
|
|
FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.NewProject.ProjectCreated" ), EventAttributes );
|
|
}
|
|
|
|
return bProjectCreationSuccessful;
|
|
}
|
|
|
|
void GameProjectUtils::CheckForOutOfDateGameProjectFile()
|
|
{
|
|
if ( FPaths::IsProjectFilePathSet() )
|
|
{
|
|
if (IProjectManager::Get().IsCurrentProjectDirty())
|
|
{
|
|
FText FailMessage;
|
|
TryMakeProjectFileWriteable(FPaths::GetProjectFilePath());
|
|
if (!IProjectManager::Get().SaveCurrentProjectToDisk(FailMessage))
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, FailMessage);
|
|
}
|
|
}
|
|
|
|
FProjectStatus ProjectStatus;
|
|
if (IProjectManager::Get().QueryStatusForCurrentProject(ProjectStatus))
|
|
{
|
|
if ( ProjectStatus.bRequiresUpdate )
|
|
{
|
|
const FText UpdateProjectText = LOCTEXT("UpdateProjectFilePrompt", "Project file is saved in an older format. Would you like to update it?");
|
|
const FText UpdateProjectConfirmText = LOCTEXT("UpdateProjectFileConfirm", "Update");
|
|
const FText UpdateProjectCancelText = LOCTEXT("UpdateProjectFileCancel", "Not Now");
|
|
|
|
FNotificationInfo Info(UpdateProjectText);
|
|
Info.bFireAndForget = false;
|
|
Info.bUseLargeFont = false;
|
|
Info.bUseThrobber = false;
|
|
Info.bUseSuccessFailIcons = false;
|
|
Info.FadeOutDuration = 3.f;
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectConfirmText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectConfirm)));
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectCancelText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectCancel)));
|
|
|
|
if (UpdateGameProjectNotification.IsValid())
|
|
{
|
|
UpdateGameProjectNotification.Pin()->ExpireAndFadeout();
|
|
UpdateGameProjectNotification.Reset();
|
|
}
|
|
|
|
UpdateGameProjectNotification = FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
if (UpdateGameProjectNotification.IsValid())
|
|
{
|
|
UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if there are any installed plugins which aren't referenced by the project file
|
|
if(!UpdateGameProjectNotification.IsValid())
|
|
{
|
|
const FProjectDescriptor* Project = IProjectManager::Get().GetCurrentProject();
|
|
if(Project != nullptr)
|
|
{
|
|
TArray<FPluginReferenceDescriptor> NewPluginReferences;
|
|
for(TSharedRef<IPlugin>& Plugin: IPluginManager::Get().GetEnabledPlugins())
|
|
{
|
|
if(Plugin->GetDescriptor().bInstalled && Project->FindPluginReferenceIndex(Plugin->GetName()) == INDEX_NONE)
|
|
{
|
|
FPluginReferenceDescriptor PluginReference(Plugin->GetName(), true, Plugin->GetDescriptor().MarketplaceURL);
|
|
NewPluginReferences.Add(PluginReference);
|
|
}
|
|
}
|
|
if(NewPluginReferences.Num() > 0)
|
|
{
|
|
UpdateProject(FProjectDescriptorModifier::CreateLambda(
|
|
[NewPluginReferences](FProjectDescriptor& Descriptor){ Descriptor.Plugins.Append(NewPluginReferences); return true; }
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameProjectUtils::CheckAndWarnProjectFilenameValid()
|
|
{
|
|
const FString& LoadedProjectFilePath = FPaths::IsProjectFilePathSet() ? FPaths::GetProjectFilePath() : FString();
|
|
if ( !LoadedProjectFilePath.IsEmpty() )
|
|
{
|
|
const FString BaseProjectFile = FPaths::GetBaseFilename(LoadedProjectFilePath);
|
|
if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH );
|
|
const FText WarningReason = FText::Format( LOCTEXT( "WarnProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters.\nYou might have problems saving or modifying a project with a longer name." ), Args );
|
|
const FText WarningReasonOkText = LOCTEXT("WarningReasonOkText", "Ok");
|
|
|
|
FNotificationInfo Info(WarningReason);
|
|
Info.bFireAndForget = false;
|
|
Info.bUseLargeFont = false;
|
|
Info.bUseThrobber = false;
|
|
Info.bUseSuccessFailIcons = false;
|
|
Info.FadeOutDuration = 3.f;
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(WarningReasonOkText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnWarningReasonOk)));
|
|
|
|
if (WarningProjectNameNotification.IsValid())
|
|
{
|
|
WarningProjectNameNotification.Pin()->ExpireAndFadeout();
|
|
WarningProjectNameNotification.Reset();
|
|
}
|
|
|
|
WarningProjectNameNotification = FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
if (WarningProjectNameNotification.IsValid())
|
|
{
|
|
WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameProjectUtils::OnWarningReasonOk()
|
|
{
|
|
if ( WarningProjectNameNotification.IsValid() )
|
|
{
|
|
WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_None);
|
|
WarningProjectNameNotification.Pin()->ExpireAndFadeout();
|
|
WarningProjectNameNotification.Reset();
|
|
}
|
|
}
|
|
|
|
bool GameProjectUtils::UpdateStartupModuleNames(FProjectDescriptor& Descriptor, const TArray<FString>* StartupModuleNames)
|
|
{
|
|
if (StartupModuleNames == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Replace the modules names, if specified
|
|
Descriptor.Modules.Empty();
|
|
for (int32 Idx = 0; Idx < StartupModuleNames->Num(); Idx++)
|
|
{
|
|
Descriptor.Modules.Add(FModuleDescriptor(*(*StartupModuleNames)[Idx]));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::UpdateRequiredAdditionalDependencies(FProjectDescriptor& Descriptor, TArray<FString>& RequiredDependencies, const FString& ModuleName)
|
|
{
|
|
bool bNeedsUpdate = false;
|
|
|
|
for (auto& ModuleDesc : Descriptor.Modules)
|
|
{
|
|
if (ModuleDesc.Name != *ModuleName)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (const auto& RequiredDep : RequiredDependencies)
|
|
{
|
|
if (!ModuleDesc.AdditionalDependencies.Contains(RequiredDep))
|
|
{
|
|
ModuleDesc.AdditionalDependencies.Add(RequiredDep);
|
|
bNeedsUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bNeedsUpdate;
|
|
}
|
|
|
|
bool GameProjectUtils::UpdateGameProject(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason)
|
|
{
|
|
return UpdateGameProjectFile(ProjectFile, EngineIdentifier, OutFailReason);
|
|
}
|
|
|
|
void GameProjectUtils::OpenAddToProjectDialog(const FAddToProjectConfig& Config, EClassDomain InDomain)
|
|
{
|
|
// If we've been given a class then we only show the second page of the dialog, so we can make the window smaller as that page doesn't have as much content
|
|
const FVector2D WindowSize = (Config._ParentClass) ? (InDomain == EClassDomain::Blueprint) ? FVector2D(940, 480) : FVector2D(940, 380) : FVector2D(940, 540);
|
|
|
|
FText WindowTitle = Config._WindowTitle;
|
|
if (WindowTitle.IsEmpty())
|
|
{
|
|
WindowTitle = InDomain == EClassDomain::Native ? LOCTEXT("AddCodeWindowHeader_Native", "Add C++ Class") : LOCTEXT("AddCodeWindowHeader_Blueprint", "Add Blueprint Class");
|
|
}
|
|
|
|
TSharedRef<SWindow> AddCodeWindow =
|
|
SNew(SWindow)
|
|
.Title( WindowTitle )
|
|
.ClientSize( WindowSize )
|
|
.SizingRule( ESizingRule::FixedSize )
|
|
.SupportsMinimize(false) .SupportsMaximize(false);
|
|
|
|
TSharedRef<SNewClassDialog> NewClassDialog =
|
|
SNew(SNewClassDialog)
|
|
.Class(Config._ParentClass)
|
|
.ClassViewerFilter(Config._AllowableParents)
|
|
.ClassDomain(InDomain)
|
|
.FeaturedClasses(Config._FeaturedClasses)
|
|
.InitialPath(Config._InitialPath)
|
|
.OnAddedToProject( Config._OnAddedToProject )
|
|
.DefaultClassPrefix( Config._DefaultClassPrefix )
|
|
.DefaultClassName( Config._DefaultClassName );
|
|
|
|
AddCodeWindow->SetContent( NewClassDialog );
|
|
|
|
TSharedPtr<SWindow> ParentWindow = Config._ParentWindow;
|
|
if (!ParentWindow.IsValid())
|
|
{
|
|
static const FName MainFrameModuleName = "MainFrame";
|
|
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(MainFrameModuleName);
|
|
ParentWindow = MainFrameModule.GetParentWindow();
|
|
}
|
|
|
|
if (Config._bModal)
|
|
{
|
|
FSlateApplication::Get().AddModalWindow(AddCodeWindow, ParentWindow);
|
|
}
|
|
else if (ParentWindow.IsValid())
|
|
{
|
|
FSlateApplication::Get().AddWindowAsNativeChild(AddCodeWindow, ParentWindow.ToSharedRef());
|
|
}
|
|
else
|
|
{
|
|
FSlateApplication::Get().AddWindow(AddCodeWindow);
|
|
}
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, FText& OutFailReason)
|
|
{
|
|
if ( NewClassName.IsEmpty() )
|
|
{
|
|
OutFailReason = LOCTEXT( "NoClassName", "You must specify a class name." );
|
|
return false;
|
|
}
|
|
|
|
if ( NewClassName.Contains(TEXT(" ")) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ClassNameContainsSpace", "Your class name may not contain a space." );
|
|
return false;
|
|
}
|
|
|
|
if ( !FChar::IsAlpha(NewClassName[0]) )
|
|
{
|
|
OutFailReason = LOCTEXT( "ClassNameMustBeginWithACharacter", "Your class name must begin with an alphabetic character." );
|
|
return false;
|
|
}
|
|
|
|
if ( NewClassName.Len() > MAX_CLASS_NAME_LENGTH )
|
|
{
|
|
OutFailReason = FText::Format( LOCTEXT( "ClassNameTooLong", "The class name must not be longer than {0} characters." ), FText::AsNumber(MAX_CLASS_NAME_LENGTH) );
|
|
return false;
|
|
}
|
|
|
|
FString IllegalNameCharacters;
|
|
if ( !NameContainsOnlyLegalCharacters(NewClassName, IllegalNameCharacters) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) );
|
|
OutFailReason = FText::Format( LOCTEXT( "ClassNameContainsIllegalCharacters", "The class name may not contain the following characters: {IllegalNameCharacters}" ), Args );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, const FModuleContextInfo& ModuleInfo, const TSet<FString>& DisallowedHeaderNames, FText& OutFailReason)
|
|
{
|
|
if (!IsValidClassNameForCreation(NewClassName, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Look for a duplicate class in memory
|
|
for ( TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt )
|
|
{
|
|
if ( ClassIt->GetName() == NewClassName )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) );
|
|
OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Look for a duplicate class on disk in their project
|
|
{
|
|
FString UnusedFoundPath;
|
|
if ( FindSourceFileInProject(NewClassName + ".h", ModuleInfo.ModuleSourcePath, UnusedFoundPath) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) );
|
|
OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// See if header name clashes with an engine header
|
|
{
|
|
FString UnusedFoundPath;
|
|
if (DisallowedHeaderNames.Contains(NewClassName))
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("NewHeaderName"), FText::FromString(NewClassName + ".h"));
|
|
OutFailReason = FText::Format(LOCTEXT("HeaderNameAlreadyExists", "The file {NewHeaderName} already exists elsewhere in the engine."), Args);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const FModuleContextInfo& InModuleInfo)
|
|
{
|
|
auto DoesClassNeedAPIExport = [&InModuleInfo](const FString& InClassModuleName) -> bool
|
|
{
|
|
return InModuleInfo.ModuleName != InClassModuleName;
|
|
};
|
|
|
|
return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport));
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const TArray<FModuleContextInfo>& InModuleInfoArray)
|
|
{
|
|
auto DoesClassNeedAPIExport = [&InModuleInfoArray](const FString& InClassModuleName) -> bool
|
|
{
|
|
for(const FModuleContextInfo& ModuleInfo : InModuleInfoArray)
|
|
{
|
|
if(ModuleInfo.ModuleName == InClassModuleName)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport));
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidBaseClassForCreation_Internal(const UClass* InClass, const FDoesClassNeedAPIExportCallback& InDoesClassNeedAPIExport)
|
|
{
|
|
// You may not make native classes based on blueprint generated classes
|
|
const bool bIsBlueprintClass = (InClass->ClassGeneratedBy != nullptr);
|
|
|
|
// UObject is special cased to be extensible since it would otherwise not be since it doesn't pass the API check (intrinsic class).
|
|
const bool bIsExplicitlyUObject = (InClass == UObject::StaticClass());
|
|
|
|
// You need API if you are not UObject itself, and you're in a module that was validated as needing API export
|
|
const FString ClassModuleName = InClass->GetOutermost()->GetName().RightChop( FString(TEXT("/Script/")).Len() );
|
|
const bool bNeedsAPI = !bIsExplicitlyUObject && InDoesClassNeedAPIExport.Execute(ClassModuleName);
|
|
|
|
// You may not make a class that is not DLL exported.
|
|
// MinimalAPI classes aren't compatible with the DLL export macro, but can still be used as a valid base
|
|
const bool bHasAPI = InClass->HasAnyClassFlags(CLASS_RequiredAPI) || InClass->HasAnyClassFlags(CLASS_MinimalAPI);
|
|
|
|
// @todo should we support interfaces?
|
|
const bool bIsInterface = InClass->IsChildOf(UInterface::StaticClass());
|
|
|
|
return !bIsBlueprintClass && (!bNeedsAPI || bHasAPI) && !bIsInterface;
|
|
}
|
|
|
|
GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet<FString>& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason)
|
|
{
|
|
const EAddCodeToProjectResult Result = AddCodeToProject_Internal(NewClassName, NewClassPath, ModuleInfo, ParentClassInfo, DisallowedHeaderNames, OutHeaderFilePath, OutCppFilePath, OutFailReason);
|
|
|
|
if( FEngineAnalytics::IsAvailable() )
|
|
{
|
|
const FString ParentClassName = ParentClassInfo.GetClassNameCPP();
|
|
|
|
TArray<FAnalyticsEventAttribute> EventAttributes;
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ParentClass"), ParentClassName.IsEmpty() ? TEXT("None") : ParentClassName));
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), Result == EAddCodeToProjectResult::Succeeded ? TEXT("Successful") : TEXT("Failed")));
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("FailureReason"), OutFailReason.ToString()));
|
|
|
|
FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.AddCodeToProject.CodeAdded" ), EventAttributes );
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
UTemplateProjectDefs* GameProjectUtils::LoadTemplateDefs(const FString& ProjectDirectory)
|
|
{
|
|
UTemplateProjectDefs* TemplateDefs = NULL;
|
|
|
|
const FString TemplateDefsIniFilename = ProjectDirectory / TEXT("Config") / GetTemplateDefsFilename();
|
|
if ( FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateDefsIniFilename) )
|
|
{
|
|
UClass* ClassToConstruct = UDefaultTemplateProjectDefs::StaticClass();
|
|
|
|
// see if template uses a custom project defs object
|
|
FString ClassName;
|
|
const bool bFoundValue = GConfig->GetString(*UTemplateProjectDefs::StaticClass()->GetPathName(), TEXT("TemplateProjectDefsClass"), ClassName, TemplateDefsIniFilename);
|
|
if (bFoundValue && ClassName.Len() > 0)
|
|
{
|
|
UClass* OverrideClass = FindObject<UClass>(ANY_PACKAGE, *ClassName, false);
|
|
if (nullptr != OverrideClass)
|
|
{
|
|
ClassToConstruct = OverrideClass;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogGameProjectGeneration, Error, TEXT("Failed to find template project defs class '%s', using default."), *ClassName);
|
|
}
|
|
}
|
|
TemplateDefs = NewObject<UTemplateProjectDefs>(GetTransientPackage(), ClassToConstruct);
|
|
TemplateDefs->LoadConfig(UTemplateProjectDefs::StaticClass(), *TemplateDefsIniFilename);
|
|
}
|
|
|
|
return TemplateDefs;
|
|
}
|
|
|
|
bool GameProjectUtils::GenerateProjectFromScratch(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog)
|
|
{
|
|
FScopedSlowTask SlowTask(5);
|
|
|
|
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
|
|
const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
|
|
TArray<FString> CreatedFiles;
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
// Generate config files
|
|
if (!GenerateConfigFiles(InProjectInfo, CreatedFiles, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Insert any required feature packs (EG starter content) into ini file. These will be imported automatically when the editor is first run
|
|
if(!InsertFeaturePacksIntoINIFile(InProjectInfo, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Make the Content folder
|
|
const FString ContentFolder = NewProjectFolder / TEXT("Content");
|
|
if ( !IFileManager::Get().MakeDirectory(*ContentFolder) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("ContentFolder"), FText::FromString( ContentFolder ) );
|
|
OutFailReason = FText::Format( LOCTEXT("FailedToCreateContentFolder", "Failed to create the content folder {ContentFolder}"), Args );
|
|
return false;
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
TArray<FString> StartupModuleNames;
|
|
if ( InProjectInfo.bShouldGenerateCode )
|
|
{
|
|
FScopedSlowTask LocalScope(2);
|
|
|
|
LocalScope.EnterProgressFrame();
|
|
// Generate basic source code files
|
|
if ( !GenerateBasicSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, NewProjectFolder, StartupModuleNames, CreatedFiles, OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LocalScope.EnterProgressFrame();
|
|
// Generate game framework source code files
|
|
if ( !GenerateGameFrameworkSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, CreatedFiles, OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
// Generate the project file
|
|
{
|
|
// Set up the descriptor
|
|
FProjectDescriptor Descriptor;
|
|
for(int32 Idx = 0; Idx < StartupModuleNames.Num(); Idx++)
|
|
{
|
|
Descriptor.Modules.Add(FModuleDescriptor(*StartupModuleNames[Idx]));
|
|
}
|
|
|
|
// Try to save it
|
|
FText LocalFailReason;
|
|
if(!Descriptor.Save(InProjectInfo.ProjectFilename, LocalFailReason))
|
|
{
|
|
OutFailReason = LocalFailReason;
|
|
return false;
|
|
}
|
|
CreatedFiles.Add(InProjectInfo.ProjectFilename);
|
|
|
|
// Set the engine identifier for it. Do this after saving, so it can be correctly detected as foreign or non-foreign.
|
|
if(!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
if ( InProjectInfo.bShouldGenerateCode )
|
|
{
|
|
// Generate project files
|
|
if ( !GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
UE_LOG(LogGameProjectGeneration, Log, TEXT("Created new project with %d files (plus project files)"), CreatedFiles.Num());
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::CreateProjectFromTemplate(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog,TArray<FString>* OutCreatedFiles)
|
|
{
|
|
FScopedSlowTask SlowTask(10);
|
|
|
|
const FString ProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
|
|
const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
|
|
const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile);
|
|
const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
|
|
|
|
if ( !FPlatformFileManager::Get().GetPlatformFile().FileExists(*InProjectInfo.TemplateFile) )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("TemplateFile"), FText::FromString( InProjectInfo.TemplateFile ) );
|
|
OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingProject", "Template project \"{TemplateFile}\" does not exist."), Args );
|
|
return false;
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder);
|
|
if ( TemplateDefs == NULL )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("TemplateFile"), FText::FromString( FPaths::GetBaseFilename(InProjectInfo.TemplateFile) ) );
|
|
Args.Add( TEXT("TemplateDefinesFile"), FText::FromString( GetTemplateDefsFilename() ) );
|
|
OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingDefs", "Template project \"{TemplateFile}\" does not have definitions file: '{TemplateDefinesFile}'."), Args );
|
|
return false;
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
// Fix up the replacement strings using the specified project name
|
|
TemplateDefs->FixupStrings(TemplateName, ProjectName);
|
|
|
|
// Form a list of all extensions we care about
|
|
TSet<FString> ReplacementsInFilesExtensions;
|
|
for ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
|
|
{
|
|
ReplacementsInFilesExtensions.Append((*ReplacementIt).Extensions);
|
|
}
|
|
|
|
// Keep a list of created files so we can delete them if project creation fails
|
|
TArray<FString> CreatedFiles;
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
// Discover and copy all files in the src folder to the destination, excluding a few files and folders
|
|
TArray<FString> FilesToCopy;
|
|
TArray<FString> FilesThatNeedContentsReplaced;
|
|
TMap<FString, FString> ClassRenames;
|
|
IFileManager::Get().FindFilesRecursive(FilesToCopy, *SrcFolder, TEXT("*"), /*Files=*/true, /*Directories=*/false);
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
// Open a new feedback scope for the loop so we can report how far through the copy we are
|
|
FScopedSlowTask InnerSlowTask(FilesToCopy.Num());
|
|
for ( auto FileIt = FilesToCopy.CreateConstIterator(); FileIt; ++FileIt )
|
|
{
|
|
const FString SrcFilename = (*FileIt);
|
|
|
|
// Update the progress
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("SrcFilename"), FText::FromString( FPaths::GetCleanFilename(SrcFilename) ) );
|
|
InnerSlowTask.EnterProgressFrame(1, FText::Format( LOCTEXT( "CreatingProjectStatus_CopyingFile", "Copying File {SrcFilename}..." ), Args ));
|
|
|
|
// Get the file path, relative to the src folder
|
|
const FString SrcFileSubpath = SrcFilename.RightChop(SrcFolder.Len() + 1);
|
|
|
|
// Skip any files that were configured to be ignored
|
|
bool bThisFileIsIgnored = false;
|
|
for ( auto IgnoreIt = TemplateDefs->FilesToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt )
|
|
{
|
|
if ( SrcFileSubpath == *IgnoreIt )
|
|
{
|
|
// This file was marked as "ignored"
|
|
bThisFileIsIgnored = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( bThisFileIsIgnored )
|
|
{
|
|
// This file was marked as "ignored"
|
|
continue;
|
|
}
|
|
|
|
// Skip any folders that were configured to be ignored
|
|
bool bThisFolderIsIgnored = false;
|
|
for ( auto IgnoreIt = TemplateDefs->FoldersToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt )
|
|
{
|
|
if ( SrcFileSubpath.StartsWith((*IgnoreIt) + TEXT("/") ) )
|
|
{
|
|
// This folder was marked as "ignored"
|
|
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Skipping as it is in an ignored folder '%s'"), *SrcFilename, **IgnoreIt);
|
|
bThisFolderIsIgnored = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( bThisFolderIsIgnored )
|
|
{
|
|
// This folder was marked as "ignored"
|
|
continue;
|
|
}
|
|
|
|
// Retarget any folders that were chosen to be renamed by choosing a new destination subpath now
|
|
FString DestFileSubpathWithoutFilename = FPaths::GetPath(SrcFileSubpath) + TEXT("/");
|
|
for ( auto RenameIt = TemplateDefs->FolderRenames.CreateConstIterator(); RenameIt; ++RenameIt )
|
|
{
|
|
const FTemplateFolderRename& FolderRename = *RenameIt;
|
|
if ( SrcFileSubpath.StartsWith(FolderRename.From + TEXT("/")) )
|
|
{
|
|
// This was a file in a renamed folder. Retarget to the new location
|
|
DestFileSubpathWithoutFilename = FolderRename.To / DestFileSubpathWithoutFilename.RightChop( FolderRename.From.Len() );
|
|
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Moving to '%s' as it matched folder rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *FolderRename.From, *FolderRename.To);
|
|
}
|
|
}
|
|
|
|
// Retarget any files that were chosen to have parts of their names replaced here
|
|
FString DestBaseFilename = FPaths::GetBaseFilename(SrcFileSubpath);
|
|
const FString FileExtension = FPaths::GetExtension(SrcFileSubpath);
|
|
for ( auto ReplacementIt = TemplateDefs->FilenameReplacements.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
|
|
{
|
|
const FTemplateReplacement& Replacement = *ReplacementIt;
|
|
if ( Replacement.Extensions.Contains( FileExtension ) )
|
|
{
|
|
// This file matched a filename replacement extension, apply it now
|
|
FString LastDestBaseFilename = DestBaseFilename;
|
|
DestBaseFilename = DestBaseFilename.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);
|
|
|
|
if (LastDestBaseFilename != DestBaseFilename)
|
|
{
|
|
UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Renaming to '%s/%s' as it matched file rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *DestBaseFilename, *Replacement.From, *Replacement.To);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Perform the copy
|
|
const FString DestFilename = DestFolder / DestFileSubpathWithoutFilename + DestBaseFilename + TEXT(".") + FileExtension;
|
|
if ( IFileManager::Get().Copy(*DestFilename, *SrcFilename) == COPY_OK )
|
|
{
|
|
CreatedFiles.Add(DestFilename);
|
|
|
|
if ( ReplacementsInFilesExtensions.Contains(FileExtension) )
|
|
{
|
|
FilesThatNeedContentsReplaced.Add(DestFilename);
|
|
}
|
|
|
|
// Allow project template to extract class renames from this file copy
|
|
if (FPaths::GetBaseFilename(SrcFilename) != FPaths::GetBaseFilename(DestFilename)
|
|
&& TemplateDefs->IsClassRename(DestFilename, SrcFilename, FileExtension))
|
|
{
|
|
// Looks like a UObject file!
|
|
ClassRenames.Add(FPaths::GetBaseFilename(SrcFilename), FPaths::GetBaseFilename(DestFilename));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FFormatNamedArguments FailArgs;
|
|
FailArgs.Add(TEXT("SrcFilename"), FText::FromString(SrcFilename));
|
|
FailArgs.Add(TEXT("DestFilename"), FText::FromString(DestFilename));
|
|
OutFailReason = FText::Format(LOCTEXT("FailedToCopyFile", "Failed to copy \"{SrcFilename}\" to \"{DestFilename}\"."), FailArgs);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
// Open a new feedback scope for the loop so we can report how far through the process we are
|
|
FScopedSlowTask InnerSlowTask(FilesThatNeedContentsReplaced.Num());
|
|
|
|
// Open all files with the specified extensions and replace text
|
|
for ( auto FileIt = FilesThatNeedContentsReplaced.CreateConstIterator(); FileIt; ++FileIt )
|
|
{
|
|
InnerSlowTask.EnterProgressFrame();
|
|
|
|
const FString FileToFix = *FileIt;
|
|
bool bSuccessfullyProcessed = false;
|
|
|
|
FString FileContents;
|
|
if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) )
|
|
{
|
|
for ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt )
|
|
{
|
|
const FTemplateReplacement& Replacement = *ReplacementIt;
|
|
if ( Replacement.Extensions.Contains( FPaths::GetExtension(FileToFix) ) )
|
|
{
|
|
FileContents = FileContents.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase);
|
|
}
|
|
}
|
|
|
|
if ( FFileHelper::SaveStringToFile(FileContents, *FileToFix) )
|
|
{
|
|
bSuccessfullyProcessed = true;
|
|
}
|
|
}
|
|
|
|
if ( !bSuccessfullyProcessed )
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add( TEXT("FileToFix"), FText::FromString( FileToFix ) );
|
|
OutFailReason = FText::Format( LOCTEXT("FailedToFixUpFile", "Failed to process file \"{FileToFix}\"."), Args );
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
const FString ProjectConfigPath = DestFolder / TEXT("Config");
|
|
|
|
// Write out the hardware class target settings chosen for this project
|
|
{
|
|
const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini");
|
|
|
|
FString FileContents;
|
|
// Load the existing file - if it doesn't exist we create it
|
|
FFileHelper::LoadFileToString(FileContents, *DefaultEngineIniFilename);
|
|
|
|
FileContents += LINE_TERMINATOR;
|
|
FileContents += GetHardwareConfigString(InProjectInfo);
|
|
|
|
if ( !WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Fixup specific ini values
|
|
TArray<FTemplateConfigValue> ConfigValuesToSet;
|
|
TemplateDefs->AddConfigValues(ConfigValuesToSet, TemplateName, ProjectName, InProjectInfo.bShouldGenerateCode);
|
|
new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), FGuid::NewGuid().ToString(), /*InShouldReplaceExistingValue=*/true);
|
|
|
|
// Add all classname fixups
|
|
for ( auto RenameIt = ClassRenames.CreateConstIterator(); RenameIt; ++RenameIt )
|
|
{
|
|
const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *RenameIt.Key(), *RenameIt.Value());
|
|
new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false);
|
|
}
|
|
|
|
// Fix all specified config values
|
|
for ( auto ConfigIt = ConfigValuesToSet.CreateConstIterator(); ConfigIt; ++ConfigIt )
|
|
{
|
|
const FTemplateConfigValue& ConfigValue = *ConfigIt;
|
|
const FString IniFilename = ProjectConfigPath / ConfigValue.ConfigFile;
|
|
bool bSuccessfullyProcessed = false;
|
|
|
|
TArray<FString> FileLines;
|
|
if ( FFileHelper::LoadANSITextFileToStrings(*IniFilename, &IFileManager::Get(), FileLines) )
|
|
{
|
|
FString FileOutput;
|
|
const FString TargetSection = ConfigValue.ConfigSection;
|
|
FString CurSection;
|
|
bool bFoundTargetKey = false;
|
|
for ( auto LineIt = FileLines.CreateConstIterator(); LineIt; ++LineIt )
|
|
{
|
|
FString Line = *LineIt;
|
|
Line.Trim().TrimTrailing();
|
|
|
|
bool bShouldExcludeLineFromOutput = false;
|
|
|
|
// If we not yet found the target key parse each line looking for it
|
|
if ( !bFoundTargetKey )
|
|
{
|
|
// Check for an empty line. No work needs to be done on these lines
|
|
if ( Line.Len() == 0 )
|
|
{
|
|
|
|
}
|
|
// Comment lines start with ";". Skip these lines entirely.
|
|
else if ( Line.StartsWith(TEXT(";")) )
|
|
{
|
|
|
|
}
|
|
// If this is a section line, update the section
|
|
else if ( Line.StartsWith(TEXT("[")) )
|
|
{
|
|
// If we are entering a new section and we have not yet found our key in the target section, add it to the end of the section
|
|
if ( CurSection == TargetSection )
|
|
{
|
|
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR + LINE_TERMINATOR;
|
|
bFoundTargetKey = true;
|
|
}
|
|
|
|
// Update the current section
|
|
CurSection = Line.Mid(1, Line.Len() - 2);
|
|
}
|
|
// This is possibly an actual key/value pair
|
|
else if ( CurSection == TargetSection )
|
|
{
|
|
// Key value pairs contain an equals sign
|
|
const int32 EqualsIdx = Line.Find(TEXT("="));
|
|
if ( EqualsIdx != INDEX_NONE )
|
|
{
|
|
// Determine the key and see if it is the target key
|
|
const FString Key = Line.Left(EqualsIdx);
|
|
if ( Key == ConfigValue.ConfigKey )
|
|
{
|
|
// Found the target key, add it to the output and skip the current line if the target value is supposed to replace
|
|
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR;
|
|
bShouldExcludeLineFromOutput = ConfigValue.bShouldReplaceExistingValue;
|
|
bFoundTargetKey = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unless we replaced the key, add this line to the output
|
|
if ( !bShouldExcludeLineFromOutput )
|
|
{
|
|
FileOutput += Line;
|
|
if ( LineIt.GetIndex() < FileLines.Num() - 1 )
|
|
{
|
|
// Add a line terminator on every line except the last
|
|
FileOutput += LINE_TERMINATOR;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the key did not exist, add it here
|
|
if ( !bFoundTargetKey )
|
|
{
|
|
// If we did not end in the correct section, add the section to the bottom of the file
|
|
if ( CurSection != TargetSection )
|
|
{
|
|
FileOutput += LINE_TERMINATOR;
|
|
FileOutput += LINE_TERMINATOR;
|
|
FileOutput += FString::Printf(TEXT("[%s]"), *TargetSection) + LINE_TERMINATOR;
|
|
}
|
|
|
|
// Add the key/value here
|
|
FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR;
|
|
}
|
|
|
|
if ( FFileHelper::SaveStringToFile(FileOutput, *IniFilename) )
|
|
{
|
|
bSuccessfullyProcessed = true;
|
|
}
|
|
}
|
|
|
|
if ( !bSuccessfullyProcessed )
|
|
{
|
|
OutFailReason = LOCTEXT("FailedToFixUpDefaultEngine", "Failed to process file DefaultEngine.ini");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Insert any required feature packs (EG starter content) into ini file. These will be imported automatically when the editor is first run
|
|
if(!InsertFeaturePacksIntoINIFile(InProjectInfo, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if( AddSharedContentToProject(InProjectInfo, CreatedFiles, OutFailReason ) == false )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
// Generate the project file
|
|
{
|
|
// Load the source project
|
|
FProjectDescriptor Project;
|
|
if(!Project.Load(InProjectInfo.TemplateFile, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Update it to current
|
|
Project.EngineAssociation.Empty();
|
|
Project.EpicSampleNameHash = 0;
|
|
|
|
// Fix up module names
|
|
const FString BaseSourceName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile);
|
|
const FString BaseNewName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
|
|
for ( auto ModuleIt = Project.Modules.CreateIterator(); ModuleIt; ++ModuleIt )
|
|
{
|
|
FModuleDescriptor& ModuleInfo = *ModuleIt;
|
|
ModuleInfo.Name = FName(*ModuleInfo.Name.ToString().Replace(*BaseSourceName, *BaseNewName));
|
|
}
|
|
|
|
// Save it to disk
|
|
if(!Project.Save(InProjectInfo.ProjectFilename, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Set the engine identifier if it's a foreign project. Do this after saving, so it can be correctly detected as foreign.
|
|
if(!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Add it to the list of created files
|
|
CreatedFiles.Add(InProjectInfo.ProjectFilename);
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
if ( InProjectInfo.bShouldGenerateCode )
|
|
{
|
|
// Generate project files
|
|
if ( !GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog) )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
if (!TemplateDefs->PostGenerateProject(DestFolder, SrcFolder, InProjectInfo.ProjectFilename, InProjectInfo.TemplateFile, InProjectInfo.bShouldGenerateCode, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if( OutCreatedFiles != nullptr )
|
|
{
|
|
OutCreatedFiles->Append(CreatedFiles);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::SetEngineAssociationForForeignProject(const FString& ProjectFileName, FText& OutFailReason)
|
|
{
|
|
if(FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFileName))
|
|
{
|
|
if(!FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier()))
|
|
{
|
|
OutFailReason = LOCTEXT("FailedToSetEngineIdentifier", "Couldn't set engine identifier for project");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
FString GameProjectUtils::GetTemplateDefsFilename()
|
|
{
|
|
return TEXT("TemplateDefs.ini");
|
|
}
|
|
|
|
bool GameProjectUtils::NameContainsOnlyLegalCharacters(const FString& TestName, FString& OutIllegalCharacters)
|
|
{
|
|
bool bContainsIllegalCharacters = false;
|
|
|
|
// Only allow alphanumeric characters in the project name
|
|
bool bFoundAlphaNumericChar = false;
|
|
for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx )
|
|
{
|
|
const FString& Char = TestName.Mid( CharIdx, 1 );
|
|
if ( !FChar::IsAlnum(Char[0]) && Char != TEXT("_") )
|
|
{
|
|
if ( !OutIllegalCharacters.Contains( Char ) )
|
|
{
|
|
OutIllegalCharacters += Char;
|
|
}
|
|
|
|
bContainsIllegalCharacters = true;
|
|
}
|
|
}
|
|
|
|
return !bContainsIllegalCharacters;
|
|
}
|
|
|
|
bool GameProjectUtils::NameContainsUnderscoreAndXB1Installed(const FString& TestName)
|
|
{
|
|
// disabled for now so people with the SDK installed can use the editor
|
|
return false;
|
|
|
|
bool bContainsIllegalCharacters = false;
|
|
|
|
// Only allow alphanumeric characters in the project name
|
|
for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx )
|
|
{
|
|
const FString& Char = TestName.Mid( CharIdx, 1 );
|
|
if ( Char == TEXT("_") )
|
|
{
|
|
const ITargetPlatform* Platform = GetTargetPlatformManager()->FindTargetPlatform(TEXT("XboxOne"));
|
|
if (Platform)
|
|
{
|
|
FString NotInstalledDocLink;
|
|
if (Platform->IsSdkInstalled(true, NotInstalledDocLink))
|
|
{
|
|
bContainsIllegalCharacters = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bContainsIllegalCharacters;
|
|
}
|
|
|
|
bool GameProjectUtils::ProjectFileExists(const FString& ProjectFile)
|
|
{
|
|
return FPlatformFileManager::Get().GetPlatformFile().FileExists(*ProjectFile);
|
|
}
|
|
|
|
bool GameProjectUtils::AnyProjectFilesExistInFolder(const FString& Path)
|
|
{
|
|
TArray<FString> ExistingFiles;
|
|
const FString Wildcard = FString::Printf(TEXT("%s/*.%s"), *Path, *FProjectDescriptor::GetExtension());
|
|
IFileManager::Get().FindFiles(ExistingFiles, *Wildcard, /*Files=*/true, /*Directories=*/false);
|
|
|
|
return ExistingFiles.Num() > 0;
|
|
}
|
|
|
|
bool GameProjectUtils::CleanupIsEnabled()
|
|
{
|
|
// Clean up files when running Rocket (unless otherwise specified on the command line)
|
|
return FParse::Param(FCommandLine::Get(), TEXT("norocketcleanup")) == false;
|
|
}
|
|
|
|
void GameProjectUtils::DeleteCreatedFiles(const FString& RootFolder, const TArray<FString>& CreatedFiles)
|
|
{
|
|
if (CleanupIsEnabled())
|
|
{
|
|
for ( auto FileToDeleteIt = CreatedFiles.CreateConstIterator(); FileToDeleteIt; ++FileToDeleteIt )
|
|
{
|
|
IFileManager::Get().Delete(**FileToDeleteIt);
|
|
}
|
|
|
|
// If the project folder is empty after deleting all the files we created, delete the directory as well
|
|
TArray<FString> RemainingFiles;
|
|
IFileManager::Get().FindFilesRecursive(RemainingFiles, *RootFolder, TEXT("*.*"), /*Files=*/true, /*Directories=*/false);
|
|
if ( RemainingFiles.Num() == 0 )
|
|
{
|
|
IFileManager::Get().DeleteDirectory(*RootFolder, /*RequireExists=*/false, /*Tree=*/true);
|
|
}
|
|
}
|
|
}
|
|
|
|
FString GameProjectUtils::GetHardwareConfigString(const FProjectInformation& InProjectInfo)
|
|
{
|
|
FString HardwareTargeting;
|
|
|
|
FString TargetHardwareAsString;
|
|
UEnum::GetValueAsString(TEXT("/Script/HardwareTargeting.EHardwareClass"), InProjectInfo.TargetedHardware, /*out*/ TargetHardwareAsString);
|
|
|
|
FString GraphicsPresetAsString;
|
|
UEnum::GetValueAsString(TEXT("/Script/HardwareTargeting.EGraphicsPreset"), InProjectInfo.DefaultGraphicsPerformance, /*out*/ GraphicsPresetAsString);
|
|
|
|
HardwareTargeting += TEXT("[/Script/HardwareTargeting.HardwareTargetingSettings]") LINE_TERMINATOR;
|
|
HardwareTargeting += FString::Printf(TEXT("TargetedHardwareClass=%s") LINE_TERMINATOR, *TargetHardwareAsString);
|
|
HardwareTargeting += FString::Printf(TEXT("DefaultGraphicsPerformance=%s") LINE_TERMINATOR, *GraphicsPresetAsString);
|
|
HardwareTargeting += LINE_TERMINATOR;
|
|
|
|
return HardwareTargeting;
|
|
}
|
|
|
|
bool GameProjectUtils::GenerateConfigFiles(const FProjectInformation& InProjectInfo, TArray<FString>& OutCreatedFiles, FText& OutFailReason)
|
|
{
|
|
const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename);
|
|
const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename);
|
|
|
|
FString ProjectConfigPath = NewProjectFolder / TEXT("Config");
|
|
|
|
// DefaultEngine.ini
|
|
{
|
|
const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini");
|
|
FString FileContents;
|
|
|
|
FileContents += TEXT("[URL]") LINE_TERMINATOR;
|
|
|
|
FileContents += GetHardwareConfigString(InProjectInfo);
|
|
FileContents += LINE_TERMINATOR;
|
|
|
|
if (InProjectInfo.bCopyStarterContent)
|
|
{
|
|
FString SpecificEditorStartupMap;
|
|
FString SpecificGameDefaultMap;
|
|
|
|
// If we have starter content packs available, specify starter map
|
|
if( IsStarterContentAvailableForNewProjects() == true )
|
|
{
|
|
if (InProjectInfo.TargetedHardware == EHardwareClass::Mobile)
|
|
{
|
|
SpecificEditorStartupMap = TEXT("/Game/MobileStarterContent/Maps/Minimal_Default");
|
|
SpecificGameDefaultMap = TEXT("/Game/MobileStarterContent/Maps/Minimal_Default");
|
|
}
|
|
else
|
|
{
|
|
SpecificEditorStartupMap = TEXT("/Game/StarterContent/Maps/Minimal_Default");
|
|
SpecificGameDefaultMap = TEXT("/Game/StarterContent/Maps/Minimal_Default");
|
|
}
|
|
}
|
|
|
|
// Write out the settings for startup map and game default map
|
|
FileContents += TEXT("[/Script/EngineSettings.GameMapsSettings]") LINE_TERMINATOR;
|
|
FileContents += FString::Printf(TEXT("EditorStartupMap=%s") LINE_TERMINATOR, *SpecificEditorStartupMap);
|
|
FileContents += FString::Printf(TEXT("GameDefaultMap=%s") LINE_TERMINATOR, *SpecificGameDefaultMap);
|
|
if (InProjectInfo.bShouldGenerateCode)
|
|
{
|
|
FileContents += FString::Printf(TEXT("GlobalDefaultGameMode=\"/Script/%s.%sGameMode\"") LINE_TERMINATOR, *NewProjectName, *NewProjectName);
|
|
}
|
|
}
|
|
|
|
if (WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason))
|
|
{
|
|
OutCreatedFiles.Add(DefaultEngineIniFilename);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// DefaultEditor.ini
|
|
{
|
|
const FString DefaultEditorIniFilename = ProjectConfigPath / TEXT("DefaultEditor.ini");
|
|
FString FileContents;
|
|
FileContents += TEXT("[EditoronlyBP]") LINE_TERMINATOR;
|
|
FileContents += TEXT("bAllowClassAndBlueprintPinMatching=true") LINE_TERMINATOR;
|
|
FileContents += TEXT("bReplaceBlueprintWithClass=true") LINE_TERMINATOR;
|
|
FileContents += TEXT("bDontLoadBlueprintOutsideEditor=true") LINE_TERMINATOR;
|
|
FileContents += TEXT("bBlueprintIsNotBlueprintType=true") LINE_TERMINATOR;
|
|
|
|
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
|