diff --git a/Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template b/Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template index 6b310cdd2b9a..aa900aa45496 100644 --- a/Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template +++ b/Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java.template @@ -41,6 +41,7 @@ import java.text.DecimalFormatSymbols; import java.util.zip.Inflater; import java.util.Timer; import java.util.TimerTask; +import java.lang.Runnable; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -75,6 +76,7 @@ import android.util.Log; import android.os.Vibrator; import android.os.SystemClock; +import android.os.Looper; import android.os.Handler; import android.os.HandlerThread; @@ -113,6 +115,8 @@ import android.provider.Settings; import android.provider.Settings.Secure; import android.content.pm.PackageInfo; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; import android.media.AudioManager; import android.util.DisplayMetrics; @@ -281,8 +285,9 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba static Bundle _bundle; static Bundle _extrasBundle; - private Timer memoryTimer; - private TimerTask memoryTask; + private HandlerThread memoryHandlerThread; + private Handler memoryHandler; + private Runnable memoryRunnable; public int UsedMemory; public String InternalFilesDir; @@ -2020,7 +2025,7 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba _activity = this; // update memory stats every 10 seconds - memoryTask = new TimerTask() { + memoryRunnable = new Runnable() { @Override public void run() { @@ -2048,11 +2053,17 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba synchronized(_activity) { _activity.UsedMemory = ProcessMemory; + if (_activity.memoryHandler != null) + { + _activity.memoryHandler.postDelayed(this, 10000); + } } } }; - memoryTimer = new Timer(); - memoryTimer.schedule(memoryTask, 0, 10000); + memoryHandlerThread = new HandlerThread("MemoryUsageThread", android.os.Process.THREAD_PRIORITY_LOWEST); + memoryHandlerThread.start(); + memoryHandler = new Handler(memoryHandlerThread.getLooper()); + memoryHandler.postDelayed(memoryRunnable, 1); // layout required by popups, e.g ads, native controls MarginLayoutParams params = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); @@ -2828,6 +2839,17 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba { super.onResume(); + // start memory reporter timer (runs every 10 seconds) + synchronized(this) + { + if (memoryHandler == null && memoryRunnable != null) + { + Log.debug("onResume: start memory reporter runnable"); + memoryHandler = new Handler(memoryHandlerThread.getLooper()); + memoryHandler.postDelayed(memoryRunnable, 1000); + } + } + // only do this on KitKat and above if (ShouldHideUI) { @@ -2949,7 +2971,16 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba @Override protected void onPause() { - super.onPause(); + // stop memory reporter timer if running + synchronized(this) + { + if (memoryHandler != null) + { + Log.debug("onPause: stop memory reporter runnable"); + memoryHandler.removeCallbacks(memoryRunnable); + memoryHandler = null; + } + } if (bAllowIMU) { @@ -2988,6 +3019,7 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba }); } //$${gameActivityOnPauseAdditions}$$ + super.onPause(); Log.debug("==============> GameActive.onPause complete!"); } @@ -3138,26 +3170,41 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba @Override public void onStop() { - super.onStop(); - if (consoleCmdReceiver != null) { unregisterReceiver(consoleCmdReceiver); } //$${gameActivityOnStopAdditions}$$ + super.onStop(); Log.debug("==============> GameActive.onStop complete!"); } @Override public void onDestroy() { - super.onDestroy(); + synchronized(this) + { + if (memoryHandler != null) + { + Log.debug("onDestroy: destroy memoryHandler"); + memoryHandler.removeCallbacks(memoryRunnable); + memoryHandler = null; + } + if (memoryHandlerThread != null) + { + Log.debug("onDestroy: destroy memoryHandlerThread"); + memoryHandlerThread.quit(); + memoryHandlerThread = null; + } + } + if( IapStoreHelper != null ) { IapStoreHelper.onDestroy(); } //$${gameActivityOnDestroyAdditions}$$ + super.onDestroy(); Log.debug("==============> GameActive.onDestroy complete!"); } @@ -4458,7 +4505,10 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba new DeviceInfoData(0x045e, 0x028e, "Xbox Wired Controller"), new DeviceInfoData(0x045e, 0x02e0, "Xbox Wireless Controller"), new DeviceInfoData(0x0111, 0x1419, "SteelSeries Stratus XL"), - new DeviceInfoData(0x054c, 0x05c4, "PS4 Wireless Controller") + new DeviceInfoData(0x054c, 0x05c4, "PS4 Wireless Controller"), + new DeviceInfoData(0x054c, 0x09cc, "PS4 Wireless Controller (v2)"), + new DeviceInfoData(0x05ac, 0x056a, "glap QXPGP001"), + new DeviceInfoData(0x0483, 0x5750, "STMicroelectronics Lenovo GamePad") }; public class InputDeviceInfo { @@ -5373,6 +5423,103 @@ public class GameActivity extends NativeActivity implements SurfaceHolder.Callba return ""; } + @SuppressWarnings("deprecation") + public boolean AndroidThunkJava_CookieManager_SetCookie(String Url, String Value) + { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) + { + CookieManager cookieManager = CookieManager.getInstance(); + if (cookieManager != null) + { + cookieManager.setAcceptCookie(true); + cookieManager.setCookie(Url, Value); // could use callback for API 21+ + cookieManager.flush(); + return true; + } + } + else + { + Context context = getApplicationContext(); + CookieSyncManager syncManager = CookieSyncManager.createInstance(context); + syncManager.sync(); + + CookieManager cookieManager = CookieManager.getInstance(); + if (cookieManager != null) + { + cookieManager.setAcceptCookie(true); + + cookieManager.setCookie(Url, Value); + syncManager.sync(); + return true; + } + } + + return false; + } + + @SuppressWarnings("deprecation") + public String AndroidThunkJava_CookieManager_GetCookies(String Url) + { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) + { + Context context = getApplicationContext(); + CookieSyncManager syncManager = CookieSyncManager.createInstance(context); + syncManager.sync(); + } + + CookieManager cookieManager = CookieManager.getInstance(); + if (cookieManager != null) + { + cookieManager.setAcceptCookie(true); + return cookieManager.getCookie(Url); + } + return null; + } + + @SuppressWarnings("deprecation") + public boolean AndroidThunkJava_CookieManager_RemoveCookies(String Url) + { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) + { + Context context = getApplicationContext(); + CookieSyncManager syncManager = CookieSyncManager.createInstance(context); + syncManager.sync(); + + CookieManager cookieManager = CookieManager.getInstance(); + if (cookieManager != null) + { + cookieManager.setAcceptCookie(true); + String cookies = cookieManager.getCookie(Url); + if (cookies == null) + { + return false; + } + + cookieManager.setCookie(Url, "expires=Sat, 1 Jan 2000 00:00:01 UTC;"); + cookieManager.removeExpiredCookie(); + syncManager.sync(); + return true; + } + return false; + } + + CookieManager cookieManager = CookieManager.getInstance(); + if (cookieManager != null) + { + cookieManager.setAcceptCookie(true); + String cookies = cookieManager.getCookie(Url); + if (cookies == null) + { + return false; + } + + cookieManager.setCookie(Url, "expires=Sat, 1 Jan 2000 00:00:01 UTC;"); // could use callback for API 21+ + cookieManager.flush(); + return true; + } + return false; + } + public native int nativeGetCPUFamily(); public native boolean nativeSupportsNEON(); public native void nativeSetAffinityInfo(boolean bEnableAffinity, int bigCoreMask, int littleCoreMask); diff --git a/Engine/Build/Android/Java/src/com/epicgames/ue4/MediaPlayer14.java b/Engine/Build/Android/Java/src/com/epicgames/ue4/MediaPlayer14.java index a0dbd11f1315..3f951563a4d6 100644 --- a/Engine/Build/Android/Java/src/com/epicgames/ue4/MediaPlayer14.java +++ b/Engine/Build/Android/Java/src/com/epicgames/ue4/MediaPlayer14.java @@ -10,6 +10,7 @@ import android.os.Build; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import android.media.MediaDataSource; import android.media.MediaExtractor; import android.media.MediaFormat; import android.media.MediaPlayer; @@ -284,6 +285,112 @@ public class MediaPlayer14 return true; } + public native int nativeReadAt(long identifier, long position, java.nio.ByteBuffer buffer, int offset, int size); + + public class PakDataSource extends MediaDataSource { + java.nio.ByteBuffer fileBuffer; + long identifier; + long fileSize; + + public PakDataSource(long inIdentifier, long inFileSize) + { + fileBuffer = java.nio.ByteBuffer.allocateDirect(65536); + identifier = inIdentifier; + fileSize = inFileSize; + } + + @Override + public synchronized int readAt(long position, byte[] buffer, int offset, int size) throws IOException + { + synchronized(fileBuffer) + { + //GameActivity.Log.debug("PDS: readAt(" + position + ", " + offset + ", " + size + ") bufferLen=" + buffer.length + ", fileBuffer.length=" + fileSize); + if (position >= fileSize) + { + return -1; // EOF + } + if (position + size > fileSize) + { + size = (int)(fileSize - position); + } + if (size > 0) + { + int readBytes = nativeReadAt(identifier, position, fileBuffer, 0, size); + if (readBytes > 0) + { + System.arraycopy(fileBuffer.array(), fileBuffer.arrayOffset(), buffer, offset, readBytes); + } + + return readBytes; + } + return 0; + } + } + + @Override + public synchronized long getSize() throws IOException + { + //GameActivity.Log.debug("PDS: getSize() = " + fileSize); + return fileSize; + } + + @Override + public synchronized void close() throws IOException + { + //GameActivity.Log.debug("PDS: close()"); + } + } + + public boolean setDataSourceArchive( + long identifier, long size) + throws IOException, + java.lang.InterruptedException, + java.util.concurrent.ExecutionException + { + synchronized(this) + { + Prepared = false; + Completed = false; + } + Looping = false; + AudioEnabled = true; + audioTracks.clear(); + videoTracks.clear(); + + // Android 6.0 required for MediaDataSource + if (android.os.Build.VERSION.SDK_INT < 23) + { + return false; + } + + try + { + PakDataSource dataSource = new PakDataSource(identifier, size); + setDataSource(dataSource); + + releaseOESTextureRenderer(); + releaseBitmapRenderer(); + + if (android.os.Build.VERSION.SDK_INT >= 16) + { + MediaExtractor extractor = new MediaExtractor(); + if (extractor != null) + { + extractor.setDataSource(dataSource); + updateTrackInfo(extractor); + extractor.release(); + extractor = null; + } + } + } + catch(IOException e) + { + GameActivity.Log.debug("setDataSource (archive): Exception = " + e); + return false; + } + return true; + } + public boolean setDataSource( String moviePath, long offset, long size) throws IOException, diff --git a/Engine/Build/BatchFiles/Mac/Build.sh b/Engine/Build/BatchFiles/Mac/Build.sh index b5b447cbcec5..0e7ad4a1275a 100755 --- a/Engine/Build/BatchFiles/Mac/Build.sh +++ b/Engine/Build/BatchFiles/Mac/Build.sh @@ -1,108 +1,17 @@ #!/bin/sh -# This script gets called every time Xcode does a build or clean operation, even though it's called "Build.sh". -# Values for $ACTION: "" = building, "clean" = cleaning +cd "`dirname "$0"`/../../../.." # Setup Mono source Engine/Build/BatchFiles/Mac/SetupMono.sh Engine/Build/BatchFiles/Mac -# override env if action is specified on command line -if [ $1 == "clean" ] - then - ACTION="clean" -fi +if [ "$4" == "-buildscw" ] || [ "$5" == "-buildscw" ]; then + echo Building ShaderCompileWorker... + mono Engine/Binaries/DotNET/UnrealBuildTool.exe ShaderCompileWorker Mac Development +fi - -case $ACTION in - "") - echo Building $1... - - Platform="" - AdditionalFlags="" - - case $CLANG_STATIC_ANALYZER_MODE in - "deep") - AdditionalFlags+="-SkipActionHistory" - ;; - "shallow") - AdditionalFlags+="-SkipActionHistory" - ;; - esac - - case $ENABLE_THREAD_SANITIZER in - "YES"|"1") - # Disable TSAN atomic->non-atomic race reporting as we aren't C++11 memory-model conformant so UHT will fail - export TSAN_OPTIONS="suppress_equal_stacks=true suppress_equal_addresses=true report_atomic_races=false" - ;; - esac - - case $2 in - "iphoneos"|"IOS") - Platform="IOS" - AdditionalFlags+=" -deploy " - ;; - "appletvos") - Platform="TVOS" - AdditionalFlags+=" -deploy " - ;; - "iphonesimulator") - Platform="IOS" - AdditionalFlags+=" -deploy -simulator" - ;; - "macosx") - Platform="Mac" - ;; - "HTML5") - Platform="HTML5" - ;; - *) - Platform="$2" - AdditionalFlags+=" -deploy " - ;; - esac - - BuildTasks=$(defaults read com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks) - export NumUBTBuildTasks=$BuildTasks - - if [ "$4" == "-buildscw" ] || [ "$5" == "-buildscw" ]; then - echo Building ShaderCompileWorker... - mono Engine/Binaries/DotNET/UnrealBuildTool.exe ShaderCompileWorker Mac Development - fi - - echo Running command : Engine/Binaries/DotNET/UnrealBuildTool.exe $1 $Platform $3 $AdditionalFlags "${@:4}" - mono Engine/Binaries/DotNET/UnrealBuildTool.exe $1 $Platform $3 $AdditionalFlags "${@:4}" - ;; - "clean") - echo "Cleaning $2 $3 $4..." - - Platform="" - AdditionalFlags="-clean" - - case $3 in - "iphoneos"|"IOS") - Platform="IOS" - AdditionalFlags+=" " - ;; - "iphonesimulator") - Platform="IOS" - AdditionalFlags+=" -simulator" - AdditionalFlags+=" " - ;; - "macosx") - Platform="Mac" - ;; - "HTML5") - Platform="HTML5" - ;; - *) - Platform="$3" - ;; - - esac - echo Running command: mono Engine/Binaries/DotNET/UnrealBuildTool.exe $2 $Platform $4 $AdditionalFlags "${@:5}" - mono Engine/Binaries/DotNET/UnrealBuildTool.exe $2 $Platform $4 $AdditionalFlags "${@:5}" - ;; -esac +echo Running command : Engine/Binaries/DotNET/UnrealBuildTool.exe "$@" +mono Engine/Binaries/DotNET/UnrealBuildTool.exe "$@" ExitCode=$? if [ $ExitCode -eq 254 ] || [ $ExitCode -eq 255 ] || [ $ExitCode -eq 2 ]; then diff --git a/Engine/Build/BatchFiles/Mac/XcodeBuild.sh b/Engine/Build/BatchFiles/Mac/XcodeBuild.sh new file mode 100755 index 000000000000..b5b447cbcec5 --- /dev/null +++ b/Engine/Build/BatchFiles/Mac/XcodeBuild.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# This script gets called every time Xcode does a build or clean operation, even though it's called "Build.sh". +# Values for $ACTION: "" = building, "clean" = cleaning + +# Setup Mono +source Engine/Build/BatchFiles/Mac/SetupMono.sh Engine/Build/BatchFiles/Mac + +# override env if action is specified on command line +if [ $1 == "clean" ] + then + ACTION="clean" +fi + + +case $ACTION in + "") + echo Building $1... + + Platform="" + AdditionalFlags="" + + case $CLANG_STATIC_ANALYZER_MODE in + "deep") + AdditionalFlags+="-SkipActionHistory" + ;; + "shallow") + AdditionalFlags+="-SkipActionHistory" + ;; + esac + + case $ENABLE_THREAD_SANITIZER in + "YES"|"1") + # Disable TSAN atomic->non-atomic race reporting as we aren't C++11 memory-model conformant so UHT will fail + export TSAN_OPTIONS="suppress_equal_stacks=true suppress_equal_addresses=true report_atomic_races=false" + ;; + esac + + case $2 in + "iphoneos"|"IOS") + Platform="IOS" + AdditionalFlags+=" -deploy " + ;; + "appletvos") + Platform="TVOS" + AdditionalFlags+=" -deploy " + ;; + "iphonesimulator") + Platform="IOS" + AdditionalFlags+=" -deploy -simulator" + ;; + "macosx") + Platform="Mac" + ;; + "HTML5") + Platform="HTML5" + ;; + *) + Platform="$2" + AdditionalFlags+=" -deploy " + ;; + esac + + BuildTasks=$(defaults read com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks) + export NumUBTBuildTasks=$BuildTasks + + if [ "$4" == "-buildscw" ] || [ "$5" == "-buildscw" ]; then + echo Building ShaderCompileWorker... + mono Engine/Binaries/DotNET/UnrealBuildTool.exe ShaderCompileWorker Mac Development + fi + + echo Running command : Engine/Binaries/DotNET/UnrealBuildTool.exe $1 $Platform $3 $AdditionalFlags "${@:4}" + mono Engine/Binaries/DotNET/UnrealBuildTool.exe $1 $Platform $3 $AdditionalFlags "${@:4}" + ;; + "clean") + echo "Cleaning $2 $3 $4..." + + Platform="" + AdditionalFlags="-clean" + + case $3 in + "iphoneos"|"IOS") + Platform="IOS" + AdditionalFlags+=" " + ;; + "iphonesimulator") + Platform="IOS" + AdditionalFlags+=" -simulator" + AdditionalFlags+=" " + ;; + "macosx") + Platform="Mac" + ;; + "HTML5") + Platform="HTML5" + ;; + *) + Platform="$3" + ;; + + esac + echo Running command: mono Engine/Binaries/DotNET/UnrealBuildTool.exe $2 $Platform $4 $AdditionalFlags "${@:5}" + mono Engine/Binaries/DotNET/UnrealBuildTool.exe $2 $Platform $4 $AdditionalFlags "${@:5}" + ;; +esac + +ExitCode=$? +if [ $ExitCode -eq 254 ] || [ $ExitCode -eq 255 ] || [ $ExitCode -eq 2 ]; then + exit 0 +else + exit $ExitCode +fi diff --git a/Engine/Build/Commit.gitdeps.xml b/Engine/Build/Commit.gitdeps.xml index 92b8268c9759..2f24e202df52 100644 --- a/Engine/Build/Commit.gitdeps.xml +++ b/Engine/Build/Commit.gitdeps.xml @@ -5,11 +5,11 @@ - - - - - + + + + + @@ -2432,23 +2432,23 @@ - + - + - + - + - + - + - + - + - + @@ -2456,135 +2456,135 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4306,6 +4306,8 @@ + + @@ -4603,6 +4605,7 @@ + @@ -7750,9 +7753,9 @@ - + - + @@ -20221,7 +20224,7 @@ - + @@ -25731,6 +25734,15 @@ + + + + + + + + + @@ -30857,12 +30869,7 @@ - - - - - @@ -31398,14 +31405,21 @@ - - - - - - - - + + + + + + + + + + + + + + + @@ -31451,7 +31465,7 @@ - + @@ -31547,7 +31561,7 @@ - + @@ -33285,6 +33299,15 @@ + + + + + + + + + @@ -33309,6 +33332,7 @@ + @@ -34734,114 +34758,114 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -34893,57 +34917,57 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -34971,33 +34995,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -35025,297 +35049,297 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -38876,6 +38900,7 @@ + @@ -38895,6 +38920,7 @@ + @@ -38918,7 +38944,9 @@ + + @@ -38950,7 +38978,7 @@ - + @@ -38971,7 +38999,7 @@ - + @@ -38979,6 +39007,7 @@ + @@ -39019,19 +39048,20 @@ - - + + - + + - + @@ -39059,6 +39089,7 @@ + @@ -39068,7 +39099,7 @@ - + @@ -39077,14 +39108,13 @@ - + - - + @@ -39099,7 +39129,7 @@ - + @@ -39107,14 +39137,13 @@ - - + @@ -39123,12 +39152,12 @@ - + @@ -39140,6 +39169,8 @@ + + @@ -39155,7 +39186,6 @@ - @@ -39164,6 +39194,7 @@ + @@ -39218,21 +39249,21 @@ - + - + - + - + @@ -39253,11 +39284,9 @@ - - @@ -39275,7 +39304,7 @@ - + @@ -39285,12 +39314,10 @@ - - @@ -39307,6 +39334,7 @@ + @@ -39318,6 +39346,7 @@ + @@ -39326,10 +39355,9 @@ - - + @@ -39345,6 +39373,7 @@ + @@ -39359,7 +39388,6 @@ - @@ -39368,24 +39396,25 @@ + + + - - - + - + @@ -39411,15 +39440,14 @@ - - + @@ -39432,7 +39460,7 @@ - + @@ -39446,13 +39474,14 @@ - + + - + @@ -39465,7 +39494,6 @@ - @@ -39512,12 +39540,11 @@ - - + @@ -39529,20 +39556,18 @@ - + - - @@ -39552,6 +39577,7 @@ + @@ -39559,7 +39585,7 @@ - + @@ -39576,6 +39602,7 @@ + @@ -39604,7 +39631,7 @@ - + @@ -39614,6 +39641,7 @@ + @@ -39621,7 +39649,7 @@ - + @@ -39645,18 +39673,20 @@ + - + + @@ -39696,7 +39726,6 @@ - @@ -39709,17 +39738,18 @@ - + - + - + + @@ -39737,11 +39767,10 @@ - - + @@ -39786,7 +39815,7 @@ - + @@ -39806,13 +39835,14 @@ - + - + + @@ -39825,7 +39855,7 @@ - + @@ -39840,9 +39870,9 @@ - + - + @@ -39870,6 +39900,7 @@ + @@ -39877,7 +39908,6 @@ - @@ -39910,7 +39940,7 @@ - + @@ -39921,9 +39951,9 @@ - + @@ -39931,8 +39961,9 @@ + - + @@ -39943,7 +39974,7 @@ - + @@ -39969,7 +40000,6 @@ - @@ -40012,7 +40042,7 @@ - + @@ -40028,9 +40058,10 @@ + - + @@ -40040,7 +40071,7 @@ - + @@ -40073,8 +40104,8 @@ + - @@ -40094,6 +40125,7 @@ + @@ -40105,7 +40137,6 @@ - @@ -40141,11 +40172,13 @@ - + + + @@ -40175,15 +40208,16 @@ - + + - + @@ -40204,7 +40238,6 @@ - @@ -40214,7 +40247,7 @@ - + @@ -40227,7 +40260,7 @@ - + @@ -40254,7 +40287,7 @@ - + @@ -40265,7 +40298,6 @@ - @@ -40274,14 +40306,13 @@ - + - @@ -40290,6 +40321,7 @@ + @@ -40301,13 +40333,13 @@ - + @@ -40331,7 +40363,7 @@ - + @@ -40344,7 +40376,6 @@ - @@ -40363,7 +40394,7 @@ - + @@ -40376,9 +40407,10 @@ - + + @@ -40387,7 +40419,6 @@ - @@ -40440,8 +40471,7 @@ - - + @@ -40462,15 +40492,17 @@ - + + + @@ -40492,6 +40524,7 @@ + @@ -40549,10 +40582,11 @@ - + + @@ -40573,7 +40607,7 @@ - + @@ -40597,7 +40631,6 @@ - @@ -40615,7 +40648,6 @@ - @@ -40647,12 +40679,11 @@ - - + @@ -40679,9 +40710,11 @@ - + + + @@ -40696,7 +40729,7 @@ - + @@ -40717,10 +40750,10 @@ - + - - + + @@ -40739,7 +40772,7 @@ - + @@ -40749,9 +40782,9 @@ + - @@ -40789,7 +40822,6 @@ - @@ -40801,15 +40833,14 @@ - - - + + - + @@ -40817,6 +40848,7 @@ + @@ -40826,10 +40858,8 @@ - - @@ -40869,7 +40899,6 @@ - @@ -40878,11 +40907,11 @@ - + - + @@ -40915,12 +40944,12 @@ - + - + @@ -40928,7 +40957,7 @@ - + @@ -40954,7 +40983,7 @@ - + @@ -40966,13 +40995,15 @@ + - + + @@ -40987,7 +41018,6 @@ - @@ -41038,10 +41068,11 @@ + - + @@ -41059,13 +41090,12 @@ - - + @@ -41087,7 +41117,7 @@ - + @@ -41096,6 +41126,7 @@ + @@ -41110,7 +41141,6 @@ - @@ -41123,6 +41153,7 @@ + @@ -41136,7 +41167,7 @@ - + @@ -41145,34 +41176,31 @@ - - - + + + - - - - + @@ -41189,31 +41217,30 @@ - - - + - + + - - + + - + @@ -41221,7 +41248,6 @@ - @@ -41262,7 +41288,7 @@ - + @@ -41279,7 +41305,7 @@ - + @@ -41288,8 +41314,7 @@ - - + @@ -41331,7 +41356,6 @@ - @@ -41339,7 +41363,6 @@ - @@ -41350,6 +41373,7 @@ + @@ -41367,7 +41391,7 @@ - + @@ -41380,6 +41404,7 @@ + @@ -41390,7 +41415,6 @@ - @@ -41399,15 +41423,15 @@ - + - + - + @@ -41430,6 +41454,7 @@ + @@ -41442,6 +41467,7 @@ + @@ -41483,11 +41509,11 @@ + - @@ -41496,13 +41522,11 @@ - - @@ -41511,6 +41535,7 @@ + @@ -41522,7 +41547,7 @@ - + @@ -41552,7 +41577,6 @@ - @@ -41581,21 +41605,20 @@ - - - + + - + @@ -41608,21 +41631,20 @@ + - - + - @@ -41637,38 +41659,37 @@ - + - + - + + - + - + - - + - @@ -41691,13 +41712,14 @@ + - + @@ -41716,7 +41738,6 @@ - @@ -41738,12 +41759,11 @@ - + - @@ -41764,7 +41784,8 @@ - + + @@ -41791,12 +41812,11 @@ - - + @@ -41848,7 +41868,7 @@ - + @@ -41861,11 +41881,11 @@ + - @@ -41879,13 +41899,13 @@ - + - + @@ -41909,10 +41929,10 @@ + - @@ -41927,8 +41947,9 @@ - + + @@ -41937,7 +41958,6 @@ - @@ -41945,6 +41965,7 @@ + @@ -41970,12 +41991,11 @@ - + - @@ -41993,7 +42013,7 @@ - + @@ -42009,7 +42029,6 @@ - @@ -42046,6 +42065,7 @@ + @@ -42075,7 +42095,7 @@ - + @@ -42093,7 +42113,7 @@ - + @@ -42108,10 +42128,11 @@ + - + @@ -42148,10 +42169,10 @@ - + - + @@ -42162,6 +42183,7 @@ + @@ -42191,7 +42213,6 @@ - @@ -42225,6 +42246,7 @@ + @@ -42237,7 +42259,7 @@ - + @@ -42247,6 +42269,7 @@ + @@ -42270,7 +42293,7 @@ - + @@ -42280,11 +42303,13 @@ - - + + + + @@ -42292,6 +42317,7 @@ + @@ -42318,7 +42344,7 @@ - + @@ -42362,28 +42388,26 @@ - + - + - + - - @@ -42402,7 +42426,7 @@ - + @@ -42413,7 +42437,7 @@ - + @@ -42433,11 +42457,10 @@ - + - @@ -42544,10 +42567,11 @@ - + + @@ -42555,7 +42579,7 @@ - + @@ -42580,10 +42604,8 @@ - - + - @@ -42598,9 +42620,8 @@ - + - @@ -42631,7 +42652,7 @@ - + @@ -42644,6 +42665,7 @@ + @@ -42656,7 +42678,7 @@ - + @@ -42676,6 +42698,7 @@ + @@ -42697,7 +42720,7 @@ - + @@ -42707,7 +42730,7 @@ - + @@ -42720,7 +42743,6 @@ - @@ -42752,8 +42774,7 @@ - - + @@ -42763,6 +42784,7 @@ + @@ -42776,7 +42798,7 @@ - + @@ -42797,8 +42819,7 @@ - - + @@ -42810,10 +42831,10 @@ - + + - @@ -42824,6 +42845,7 @@ + @@ -42847,13 +42869,13 @@ - + + - @@ -42878,6 +42900,7 @@ + @@ -42891,6 +42914,7 @@ + @@ -42918,6 +42942,7 @@ + @@ -42926,6 +42951,7 @@ + @@ -42939,11 +42965,12 @@ - + + @@ -42953,7 +42980,6 @@ - @@ -42961,10 +42987,11 @@ - + + @@ -42976,6 +43003,7 @@ + @@ -42984,7 +43012,6 @@ - @@ -43028,7 +43055,8 @@ - + + @@ -43040,7 +43068,7 @@ - + @@ -43061,7 +43089,7 @@ - + @@ -43108,7 +43136,7 @@ - + @@ -43129,7 +43157,6 @@ - @@ -43155,7 +43182,6 @@ - @@ -43172,6 +43198,7 @@ + @@ -43181,7 +43208,6 @@ - @@ -43237,7 +43263,6 @@ - @@ -43245,6 +43270,7 @@ + @@ -43265,23 +43291,22 @@ - + - + - - + @@ -43312,7 +43337,6 @@ - @@ -43337,6 +43361,7 @@ + @@ -43344,6 +43369,7 @@ + @@ -43369,7 +43395,6 @@ - @@ -43381,7 +43406,7 @@ - + @@ -43391,28 +43416,30 @@ + - + - + - + - + + @@ -43423,7 +43450,7 @@ - + @@ -43442,16 +43469,16 @@ - + - + @@ -43469,6 +43496,7 @@ + @@ -43476,7 +43504,7 @@ - + @@ -43490,9 +43518,9 @@ + - @@ -43500,6 +43528,7 @@ + @@ -43539,6 +43568,7 @@ + @@ -43554,8 +43584,9 @@ - + + @@ -43573,7 +43604,6 @@ - @@ -43587,9 +43617,9 @@ - + @@ -43601,7 +43631,7 @@ - + @@ -43612,7 +43642,6 @@ - @@ -43627,11 +43656,11 @@ + - @@ -43641,7 +43670,6 @@ - @@ -43650,7 +43678,6 @@ - @@ -43659,7 +43686,7 @@ - + @@ -43678,6 +43705,7 @@ + @@ -43702,7 +43730,7 @@ - + @@ -43715,15 +43743,15 @@ - + - + @@ -43744,7 +43772,6 @@ - @@ -43758,29 +43785,29 @@ - + - + - + - + - + @@ -43838,12 +43865,12 @@ - + - + @@ -43874,12 +43901,10 @@ - - @@ -43891,13 +43916,11 @@ - - @@ -43906,6 +43929,7 @@ + @@ -43929,6 +43953,7 @@ + @@ -43972,7 +43997,7 @@ - + @@ -43983,6 +44008,7 @@ + @@ -43995,15 +44021,15 @@ - + - + @@ -44021,8 +44047,7 @@ - - + @@ -44030,7 +44055,7 @@ - + @@ -44047,7 +44072,6 @@ - @@ -44057,11 +44081,12 @@ + - + - + @@ -44090,7 +44115,7 @@ - + @@ -44120,9 +44145,8 @@ - - + @@ -44134,6 +44158,7 @@ + @@ -44158,6 +44183,7 @@ + @@ -44183,7 +44209,6 @@ - @@ -44203,7 +44228,7 @@ - + @@ -44219,6 +44244,7 @@ + @@ -44226,20 +44252,17 @@ - - - @@ -44253,14 +44276,15 @@ + - + - + @@ -44280,7 +44304,6 @@ - @@ -44307,7 +44330,6 @@ - @@ -44333,6 +44355,7 @@ + @@ -44370,7 +44393,7 @@ - + @@ -44393,14 +44416,14 @@ - + - + @@ -44414,14 +44437,14 @@ - - + + @@ -44439,8 +44462,9 @@ - + + @@ -44451,27 +44475,24 @@ - - - + - + - @@ -44480,7 +44501,6 @@ - @@ -44498,7 +44518,7 @@ - + @@ -44528,7 +44548,7 @@ - + @@ -44551,6 +44571,7 @@ + @@ -44608,7 +44629,7 @@ - + @@ -44619,7 +44640,7 @@ - + @@ -44659,6 +44680,7 @@ + @@ -44694,7 +44716,7 @@ - + @@ -44712,7 +44734,8 @@ - + + @@ -44767,12 +44790,12 @@ - + - + @@ -44787,7 +44810,6 @@ - @@ -44795,13 +44817,12 @@ - + - @@ -44809,11 +44830,11 @@ - + - + @@ -44834,10 +44855,10 @@ + - @@ -44873,9 +44894,9 @@ + - @@ -44902,23 +44923,26 @@ + + + - + - + @@ -44927,11 +44951,12 @@ + + - @@ -44944,11 +44969,12 @@ - - + + + @@ -44958,18 +44984,19 @@ + - - + + - + @@ -44977,7 +45004,6 @@ - @@ -44994,7 +45020,6 @@ - @@ -45004,15 +45029,14 @@ - + - @@ -45034,7 +45058,6 @@ - @@ -45057,7 +45080,8 @@ - + + @@ -45074,7 +45098,7 @@ - + @@ -45107,7 +45131,7 @@ - + @@ -45120,7 +45144,6 @@ - @@ -45131,7 +45154,7 @@ - + @@ -45141,12 +45164,12 @@ - + @@ -45174,6 +45197,7 @@ + @@ -45215,9 +45239,8 @@ - - + @@ -45304,6 +45327,7 @@ + @@ -45313,6 +45337,7 @@ + @@ -45328,8 +45353,9 @@ - + + @@ -45359,7 +45385,6 @@ - @@ -45368,6 +45393,7 @@ + @@ -45375,7 +45401,6 @@ - @@ -45385,7 +45410,7 @@ - + @@ -45398,7 +45423,6 @@ - @@ -45408,8 +45432,7 @@ - - + @@ -45433,7 +45456,6 @@ - @@ -45449,11 +45471,12 @@ - + + @@ -45494,7 +45517,6 @@ - @@ -45521,7 +45543,8 @@ - + + @@ -45558,6 +45581,7 @@ + @@ -45586,16 +45610,14 @@ - + - - @@ -45608,24 +45630,24 @@ - + - - + + @@ -45648,6 +45670,7 @@ + @@ -45667,7 +45690,6 @@ - @@ -45692,7 +45714,7 @@ - + @@ -45700,7 +45722,6 @@ - @@ -45749,7 +45770,7 @@ - + @@ -45775,6 +45796,7 @@ + @@ -45782,7 +45804,7 @@ - + @@ -45790,6 +45812,7 @@ + @@ -45797,7 +45820,7 @@ - + @@ -45879,12 +45902,14 @@ + + @@ -45897,7 +45922,7 @@ - + @@ -45905,7 +45930,7 @@ - + @@ -45935,7 +45960,6 @@ - @@ -45964,10 +45988,9 @@ - - + @@ -45986,6 +46009,7 @@ + @@ -45995,8 +46019,8 @@ + - @@ -46004,7 +46028,6 @@ - @@ -46024,9 +46047,9 @@ - + - + @@ -46042,7 +46065,6 @@ - @@ -46077,17 +46099,16 @@ - + - - + @@ -46102,13 +46123,13 @@ - + @@ -46138,7 +46159,7 @@ - + @@ -46154,10 +46175,9 @@ - - + @@ -46177,13 +46197,16 @@ + + + @@ -46207,14 +46230,14 @@ - + - + @@ -46223,12 +46246,13 @@ + - + @@ -46241,7 +46265,7 @@ - + @@ -46291,13 +46315,14 @@ - + + @@ -46316,7 +46341,6 @@ - @@ -46327,12 +46351,11 @@ - + - @@ -46341,7 +46364,7 @@ - + @@ -46356,9 +46379,10 @@ + - + @@ -46371,7 +46395,6 @@ - @@ -46382,7 +46405,7 @@ - + @@ -46402,7 +46425,9 @@ + + @@ -46437,6 +46462,7 @@ + @@ -46449,9 +46475,10 @@ + - - + + @@ -46464,6 +46491,7 @@ + @@ -46488,7 +46516,6 @@ - @@ -46497,7 +46524,7 @@ - + @@ -46508,15 +46535,14 @@ - - + - + @@ -46526,6 +46552,7 @@ + @@ -46558,7 +46585,7 @@ - + @@ -46575,8 +46602,6 @@ - - @@ -46602,13 +46627,13 @@ - + @@ -46623,7 +46648,7 @@ - + @@ -46656,6 +46681,7 @@ + @@ -46679,7 +46705,6 @@ - @@ -46687,12 +46712,10 @@ - - @@ -46708,6 +46731,7 @@ + @@ -46719,7 +46743,7 @@ - + @@ -46734,13 +46758,14 @@ + + - - + @@ -46776,13 +46801,13 @@ - + - + + - @@ -46806,6 +46831,7 @@ + @@ -46822,13 +46848,12 @@ - - + + - @@ -46851,11 +46876,11 @@ - + @@ -46900,6 +46925,7 @@ + @@ -46945,7 +46971,7 @@ - + @@ -46968,11 +46994,10 @@ - - + @@ -46985,7 +47010,6 @@ - @@ -46996,27 +47020,26 @@ - + - + - + - - + @@ -47034,19 +47057,20 @@ + - - + + - + @@ -47057,9 +47081,7 @@ - - @@ -47072,6 +47094,7 @@ + @@ -47085,6 +47108,7 @@ + @@ -47100,14 +47124,14 @@ - + - + @@ -47118,7 +47142,7 @@ - + @@ -47147,6 +47171,7 @@ + @@ -47182,10 +47207,8 @@ - - @@ -47193,7 +47216,7 @@ - + @@ -47230,7 +47253,6 @@ - @@ -47260,7 +47282,6 @@ - @@ -47268,10 +47289,9 @@ - + - - + @@ -47280,6 +47300,7 @@ + @@ -47288,6 +47309,7 @@ + @@ -47306,7 +47328,7 @@ - + @@ -47332,7 +47354,7 @@ - + @@ -47340,8 +47362,9 @@ - + + @@ -47352,6 +47375,7 @@ + @@ -47388,24 +47412,24 @@ - + - + - + - + @@ -47421,6 +47445,7 @@ + @@ -47429,6 +47454,7 @@ + @@ -47472,7 +47498,6 @@ - @@ -47483,7 +47508,7 @@ - + @@ -47505,7 +47530,6 @@ - @@ -47523,7 +47547,7 @@ - + @@ -47563,7 +47587,6 @@ - @@ -47576,14 +47599,12 @@ - - - + @@ -47607,6 +47628,7 @@ + @@ -47623,12 +47645,13 @@ - + + @@ -47637,7 +47660,7 @@ - + @@ -47688,6 +47711,7 @@ + @@ -47695,7 +47719,6 @@ - @@ -47703,6 +47726,8 @@ + + @@ -47716,7 +47741,6 @@ - @@ -47728,6 +47752,7 @@ + @@ -47772,13 +47797,14 @@ + - + - - + + @@ -47814,7 +47840,6 @@ - @@ -47829,7 +47854,6 @@ - @@ -47837,12 +47861,13 @@ - + + + - @@ -47858,22 +47883,22 @@ - - + - + + - + @@ -47885,7 +47910,9 @@ - + + + @@ -47927,7 +47954,7 @@ - + @@ -47960,7 +47987,6 @@ - @@ -47969,7 +47995,6 @@ - @@ -48008,6 +48033,7 @@ + @@ -48043,7 +48069,7 @@ - + @@ -48073,8 +48099,8 @@ - - + + @@ -48124,7 +48150,6 @@ - @@ -48135,6 +48160,7 @@ + @@ -48167,7 +48193,7 @@ - + @@ -48183,7 +48209,7 @@ - + @@ -48226,7 +48252,6 @@ - @@ -48239,7 +48264,6 @@ - @@ -48293,7 +48317,6 @@ - @@ -48310,7 +48333,6 @@ - @@ -48323,10 +48345,9 @@ - + - @@ -48353,7 +48374,6 @@ - @@ -48398,15 +48418,15 @@ - + + - @@ -48436,12 +48456,11 @@ - + - @@ -48454,16 +48473,15 @@ - + - @@ -48476,7 +48494,6 @@ - @@ -48496,19 +48513,18 @@ - - + + - @@ -48518,8 +48534,7 @@ - - + @@ -48541,8 +48556,8 @@ - - + + @@ -48551,15 +48566,14 @@ - - + - + @@ -48581,12 +48595,12 @@ - - + + @@ -48599,12 +48613,11 @@ - + - @@ -48625,13 +48638,14 @@ + - + @@ -48656,7 +48670,7 @@ - + @@ -48664,6 +48678,7 @@ + @@ -48678,9 +48693,9 @@ - + @@ -48696,11 +48711,11 @@ - - + + - + @@ -48710,11 +48725,12 @@ - + + @@ -48729,7 +48745,7 @@ - + @@ -48741,12 +48757,12 @@ - + @@ -48759,15 +48775,14 @@ - + - - + @@ -48817,10 +48832,11 @@ - + + @@ -48831,7 +48847,7 @@ - + @@ -48845,7 +48861,9 @@ - + + + @@ -48860,7 +48878,6 @@ - @@ -48879,7 +48896,7 @@ - + @@ -48891,7 +48908,6 @@ - @@ -48900,6 +48916,7 @@ + @@ -48945,7 +48962,7 @@ - + @@ -48969,6 +48986,7 @@ + @@ -48988,7 +49006,7 @@ - + @@ -48998,31 +49016,29 @@ - + - - + - - + @@ -49063,6 +49079,7 @@ + @@ -49096,19 +49113,18 @@ - - + - + - + @@ -49119,10 +49135,11 @@ + - + @@ -49137,6 +49154,7 @@ + @@ -49153,26 +49171,26 @@ - + - + - + - + - + @@ -49194,7 +49212,6 @@ - @@ -49231,15 +49248,14 @@ - - + @@ -49270,13 +49286,14 @@ - + + @@ -49289,6 +49306,7 @@ + @@ -49299,7 +49317,7 @@ - + @@ -49311,7 +49329,6 @@ - @@ -49329,7 +49346,6 @@ - @@ -49362,11 +49378,11 @@ - + - + @@ -49394,8 +49410,9 @@ + - + @@ -49406,7 +49423,7 @@ - + @@ -49418,7 +49435,7 @@ - + @@ -49456,6 +49473,7 @@ + @@ -49467,7 +49485,6 @@ - @@ -49487,7 +49504,7 @@ - + @@ -49517,17 +49534,18 @@ + - + - + @@ -49543,7 +49561,7 @@ - + @@ -49554,7 +49572,7 @@ - + @@ -49586,10 +49604,9 @@ - - + @@ -49599,13 +49616,11 @@ - - @@ -49626,7 +49641,7 @@ - + @@ -49652,7 +49667,7 @@ - + @@ -49663,7 +49678,6 @@ - @@ -49703,11 +49717,11 @@ - - + + - + @@ -49726,7 +49740,7 @@ - + @@ -49770,8 +49784,8 @@ - + @@ -49785,12 +49799,13 @@ - + + @@ -49801,7 +49816,6 @@ - @@ -49815,7 +49829,7 @@ - + @@ -49834,9 +49848,7 @@ - - @@ -49876,7 +49888,6 @@ - @@ -49900,7 +49911,7 @@ - + @@ -49931,7 +49942,7 @@ - + @@ -49945,7 +49956,7 @@ - + @@ -49955,6 +49966,7 @@ + @@ -49970,7 +49982,6 @@ - @@ -49978,8 +49989,10 @@ + + @@ -49987,7 +50000,7 @@ - + @@ -50039,7 +50052,9 @@ + + @@ -50047,6 +50062,7 @@ + @@ -50054,7 +50070,7 @@ - + @@ -50065,7 +50081,7 @@ - + @@ -50108,6 +50124,7 @@ + @@ -50125,7 +50142,7 @@ - + @@ -50137,6 +50154,7 @@ + @@ -50154,7 +50172,6 @@ - @@ -50178,7 +50195,7 @@ - + @@ -50191,7 +50208,6 @@ - @@ -50219,7 +50235,6 @@ - @@ -50227,6 +50242,7 @@ + @@ -50250,12 +50266,12 @@ - + - + @@ -50264,11 +50280,10 @@ - - + @@ -50289,7 +50304,6 @@ - @@ -50323,10 +50337,12 @@ + + @@ -50350,13 +50366,12 @@ - + - @@ -50367,7 +50382,6 @@ - @@ -50375,13 +50389,12 @@ - - + @@ -50393,8 +50406,9 @@ + - + @@ -50417,6 +50431,7 @@ + @@ -50440,7 +50455,6 @@ - @@ -50448,6 +50462,7 @@ + @@ -50459,11 +50474,9 @@ - - @@ -50492,13 +50505,12 @@ - - - + + - + @@ -50512,7 +50524,6 @@ - @@ -50538,6 +50549,7 @@ + @@ -50554,7 +50566,7 @@ - + @@ -50570,6 +50582,7 @@ + @@ -50590,10 +50603,12 @@ + + @@ -50601,6 +50616,7 @@ + @@ -50626,7 +50642,7 @@ - + @@ -50644,7 +50660,7 @@ - + @@ -50685,6 +50701,7 @@ + @@ -50692,7 +50709,6 @@ - @@ -50708,25 +50724,25 @@ - + + - + - + - @@ -50748,7 +50764,6 @@ - @@ -50764,7 +50779,6 @@ - @@ -50791,9 +50805,8 @@ - - + @@ -50811,7 +50824,7 @@ - + @@ -50824,21 +50837,18 @@ - - + - - @@ -50846,6 +50856,7 @@ + @@ -50860,6 +50871,7 @@ + @@ -50881,13 +50893,13 @@ + - @@ -50908,29 +50920,30 @@ + + - - + - + @@ -50941,20 +50954,20 @@ + - + - + - @@ -50971,6 +50984,7 @@ + @@ -50998,14 +51012,13 @@ - + + - - @@ -51023,13 +51036,14 @@ + - + @@ -51040,7 +51054,7 @@ - + @@ -51059,6 +51073,7 @@ + @@ -51068,20 +51083,19 @@ - - + - + @@ -51093,7 +51107,7 @@ - + @@ -51108,7 +51122,7 @@ - + @@ -51117,6 +51131,7 @@ + @@ -51127,6 +51142,7 @@ + @@ -51134,7 +51150,7 @@ - + @@ -51170,7 +51186,7 @@ - + @@ -51202,11 +51218,11 @@ - + - + @@ -51258,6 +51274,7 @@ + @@ -51277,8 +51294,9 @@ + - + @@ -51332,7 +51350,6 @@ - @@ -51365,6 +51382,7 @@ + @@ -51374,22 +51392,22 @@ + - - + - + - + @@ -51405,6 +51423,7 @@ + @@ -51420,7 +51439,7 @@ - + @@ -51432,10 +51451,11 @@ + - + @@ -51457,7 +51477,7 @@ - + @@ -51480,7 +51500,7 @@ - + @@ -51527,24 +51547,24 @@ - - + - + + @@ -51558,6 +51578,7 @@ + @@ -51579,13 +51600,13 @@ - + - + @@ -51618,7 +51639,8 @@ - + + @@ -51630,6 +51652,7 @@ + @@ -51648,7 +51671,7 @@ - + @@ -51659,7 +51682,6 @@ - @@ -51668,7 +51690,6 @@ - @@ -51679,11 +51700,10 @@ - - + @@ -51712,16 +51732,17 @@ - + - + - + + @@ -51737,9 +51758,9 @@ - + - + @@ -51770,11 +51791,10 @@ - + - @@ -51800,6 +51820,7 @@ + @@ -51835,19 +51856,19 @@ - + - + @@ -51861,7 +51882,6 @@ - @@ -51885,7 +51905,6 @@ - @@ -51915,17 +51934,17 @@ - + + - @@ -51954,7 +51973,7 @@ - + @@ -51967,6 +51986,7 @@ + @@ -51982,17 +52002,17 @@ - - + + @@ -52009,7 +52029,6 @@ - @@ -52025,12 +52044,14 @@ + + @@ -52042,7 +52063,7 @@ - + @@ -52053,7 +52074,7 @@ - + @@ -52067,6 +52088,7 @@ + @@ -52097,7 +52119,7 @@ - + @@ -52114,6 +52136,7 @@ + @@ -52149,7 +52172,7 @@ - + @@ -52165,14 +52188,14 @@ + - + - @@ -52183,7 +52206,7 @@ - + @@ -52193,7 +52216,6 @@ - @@ -52207,7 +52229,7 @@ - + @@ -52215,7 +52237,6 @@ - @@ -52223,7 +52244,6 @@ - @@ -52233,6 +52253,7 @@ + @@ -52253,18 +52274,18 @@ - + - + @@ -52304,9 +52325,8 @@ - - + @@ -52319,9 +52339,10 @@ - + + @@ -52339,6 +52360,7 @@ + @@ -52356,7 +52378,6 @@ - @@ -52377,7 +52398,6 @@ - @@ -52427,11 +52447,13 @@ + + @@ -52443,7 +52465,6 @@ - @@ -52456,7 +52477,6 @@ - @@ -52464,6 +52484,7 @@ + @@ -52484,6 +52505,7 @@ + @@ -52492,17 +52514,16 @@ - + - + - @@ -52520,6 +52541,7 @@ + @@ -52532,7 +52554,7 @@ - + @@ -52542,12 +52564,12 @@ - + - + @@ -52605,6 +52627,7 @@ + @@ -52617,7 +52640,7 @@ - + @@ -52642,9 +52665,10 @@ - + + @@ -52662,15 +52686,14 @@ - + - + - @@ -52682,6 +52705,7 @@ + @@ -52713,6 +52737,7 @@ + @@ -52723,10 +52748,9 @@ - - + @@ -52749,14 +52773,14 @@ - + - + @@ -52775,10 +52799,12 @@ + + @@ -52810,7 +52836,6 @@ - @@ -52862,6 +52887,7 @@ + @@ -52869,6 +52895,7 @@ + @@ -52904,8 +52931,9 @@ + - + @@ -52933,6 +52961,7 @@ + @@ -52978,6 +53007,7 @@ + @@ -52994,8 +53024,7 @@ - - + @@ -53013,7 +53042,6 @@ - @@ -53023,6 +53051,7 @@ + @@ -53043,19 +53072,17 @@ - - - + - + @@ -53074,12 +53101,11 @@ - + - @@ -53095,19 +53121,19 @@ - - + + - + @@ -53134,10 +53160,10 @@ - - + + @@ -53152,6 +53178,7 @@ + @@ -53169,7 +53196,7 @@ - + @@ -53181,7 +53208,8 @@ - + + @@ -53189,9 +53217,9 @@ - + - + @@ -53200,14 +53228,12 @@ - - - + @@ -53217,13 +53243,14 @@ + - + @@ -53243,16 +53270,18 @@ - + + + @@ -53262,10 +53291,11 @@ - + + @@ -53309,10 +53339,11 @@ - - + + + @@ -53347,9 +53378,9 @@ - - + + @@ -53375,7 +53406,7 @@ - + @@ -53386,8 +53417,8 @@ - - + + @@ -53404,6 +53435,8 @@ + + @@ -53431,8 +53464,6 @@ - - @@ -53447,8 +53478,9 @@ + - + @@ -53456,6 +53488,8 @@ + + @@ -53465,6 +53499,7 @@ + @@ -53495,9 +53530,8 @@ - - + @@ -53539,15 +53573,13 @@ - - - + @@ -53571,22 +53603,22 @@ - + + - + - @@ -53605,6 +53637,7 @@ + @@ -53618,17 +53651,16 @@ + - - @@ -53637,9 +53669,9 @@ - + - + @@ -53656,10 +53688,12 @@ + + @@ -53670,6 +53704,7 @@ + @@ -53681,6 +53716,7 @@ + @@ -53702,13 +53738,12 @@ - - + - + @@ -53730,13 +53765,12 @@ - + - @@ -53758,7 +53792,6 @@ - @@ -53789,19 +53822,20 @@ - + - + - + + @@ -53826,14 +53860,13 @@ - + - @@ -53842,11 +53875,10 @@ - + - @@ -53861,7 +53893,6 @@ - @@ -53882,8 +53913,9 @@ - + + @@ -53894,10 +53926,10 @@ - + @@ -53916,6 +53948,7 @@ + @@ -53925,8 +53958,10 @@ + + @@ -53935,8 +53970,7 @@ - - + @@ -53982,7 +54016,7 @@ - + @@ -53993,9 +54027,8 @@ - + - @@ -54018,12 +54051,10 @@ - - @@ -54053,11 +54084,11 @@ - + - + @@ -54079,12 +54110,11 @@ - + - @@ -54100,7 +54130,7 @@ - + @@ -54116,7 +54146,7 @@ - + @@ -54125,6 +54155,7 @@ + @@ -54169,7 +54200,6 @@ - @@ -54181,20 +54211,17 @@ - - - - + @@ -54217,30 +54244,29 @@ + - - - + - + - + @@ -54264,7 +54290,7 @@ - + @@ -54281,7 +54307,6 @@ - @@ -54317,6 +54342,7 @@ + @@ -54324,7 +54350,7 @@ - + @@ -54335,7 +54361,7 @@ - + @@ -54366,6 +54392,7 @@ + @@ -54397,7 +54424,6 @@ - @@ -54407,6 +54433,7 @@ + @@ -54417,6 +54444,7 @@ + @@ -54424,8 +54452,7 @@ - - + @@ -54449,7 +54476,6 @@ - @@ -54480,13 +54506,14 @@ - + + @@ -54494,6 +54521,7 @@ + @@ -54509,6 +54537,7 @@ + @@ -54518,7 +54547,7 @@ - + @@ -54528,10 +54557,11 @@ - + + @@ -54554,7 +54584,7 @@ - + @@ -54565,6 +54595,7 @@ + @@ -54575,7 +54606,7 @@ - + @@ -54607,6 +54638,7 @@ + @@ -54634,6 +54666,7 @@ + @@ -54678,7 +54711,6 @@ - @@ -54686,7 +54718,9 @@ + + @@ -54702,10 +54736,9 @@ - - + @@ -54722,7 +54755,7 @@ - + @@ -54739,7 +54772,6 @@ - @@ -54753,9 +54785,10 @@ + - + @@ -54777,12 +54810,13 @@ - + + @@ -54797,13 +54831,12 @@ - + - @@ -54813,7 +54846,9 @@ + + @@ -54822,7 +54857,7 @@ - + @@ -54847,7 +54882,6 @@ - @@ -54895,19 +54929,17 @@ - - + - @@ -54926,7 +54958,7 @@ - + @@ -54934,7 +54966,7 @@ - + @@ -54952,13 +54984,13 @@ + - @@ -54986,6 +55018,7 @@ + @@ -55008,6 +55041,7 @@ + @@ -55020,13 +55054,11 @@ - + - - - + @@ -55036,10 +55068,11 @@ - + + @@ -55051,7 +55084,6 @@ - @@ -55062,7 +55094,6 @@ - @@ -55077,7 +55108,7 @@ - + @@ -55099,7 +55130,6 @@ - @@ -55107,6 +55137,7 @@ + @@ -55123,7 +55154,7 @@ - + @@ -55133,11 +55164,11 @@ + - @@ -55155,11 +55186,9 @@ - - - + @@ -55190,9 +55219,10 @@ - + - + + @@ -55276,7 +55306,6 @@ - @@ -55286,7 +55315,7 @@ - + @@ -55296,7 +55325,6 @@ - @@ -55307,6 +55335,7 @@ + @@ -55319,8 +55348,8 @@ - - + + @@ -55342,9 +55371,10 @@ + - + @@ -55355,19 +55385,19 @@ - + + - @@ -55383,8 +55413,9 @@ + - + @@ -55413,8 +55444,8 @@ - - + + @@ -55438,14 +55469,13 @@ - - + @@ -55488,6 +55518,7 @@ + @@ -55512,6 +55543,7 @@ + @@ -55562,7 +55594,7 @@ - + @@ -55573,12 +55605,11 @@ - - + @@ -55595,6 +55626,7 @@ + @@ -55617,13 +55649,12 @@ - - + @@ -55634,14 +55665,14 @@ - + + - @@ -55651,14 +55682,12 @@ - - @@ -55699,10 +55728,9 @@ - - + @@ -55710,7 +55738,7 @@ - + @@ -55730,8 +55758,8 @@ + - @@ -55776,7 +55804,6 @@ - @@ -55816,7 +55843,7 @@ - + @@ -55858,18 +55885,19 @@ - + + + - @@ -55884,7 +55912,7 @@ - + @@ -55903,12 +55931,12 @@ + - @@ -55924,16 +55952,17 @@ - - + + - + + @@ -55941,13 +55970,13 @@ + - @@ -55956,6 +55985,8 @@ + + @@ -55964,7 +55995,6 @@ - @@ -55990,14 +56020,15 @@ + - + @@ -56022,6 +56053,7 @@ + @@ -56029,21 +56061,19 @@ - - - + - + @@ -56052,6 +56082,7 @@ + @@ -56064,7 +56095,6 @@ - @@ -56072,7 +56102,6 @@ - @@ -56080,7 +56109,7 @@ - + @@ -56101,6 +56130,7 @@ + @@ -56143,14 +56173,16 @@ - + - + - + + + @@ -56161,13 +56193,11 @@ - - @@ -56182,14 +56212,14 @@ - + - + @@ -56202,28 +56232,28 @@ - - + + + - - + @@ -56276,11 +56306,12 @@ + - + @@ -56304,7 +56335,7 @@ - + @@ -56319,7 +56350,7 @@ - + @@ -56337,7 +56368,7 @@ - + @@ -56369,7 +56400,7 @@ - + @@ -56379,10 +56410,9 @@ - - + @@ -56402,12 +56432,12 @@ - + - + @@ -56426,7 +56456,7 @@ - + @@ -56438,9 +56468,10 @@ - + + @@ -56461,7 +56492,7 @@ - + @@ -56474,7 +56505,6 @@ - @@ -56483,7 +56513,7 @@ - + @@ -56496,6 +56526,7 @@ + @@ -56509,13 +56540,16 @@ + + - + + @@ -56524,6 +56558,7 @@ + @@ -56532,7 +56567,6 @@ - @@ -56540,14 +56574,14 @@ - - + + @@ -56567,7 +56601,7 @@ - + @@ -56575,8 +56609,8 @@ + - @@ -56611,10 +56645,10 @@ - + @@ -56622,6 +56656,7 @@ + @@ -56630,9 +56665,9 @@ + - @@ -56652,7 +56687,8 @@ - + + @@ -56668,7 +56704,7 @@ - + @@ -56681,9 +56717,10 @@ - + + @@ -56705,7 +56742,6 @@ - @@ -56714,9 +56750,11 @@ + - - + + + @@ -56724,9 +56762,9 @@ + - @@ -56734,7 +56772,7 @@ - + @@ -56763,6 +56801,7 @@ + @@ -56782,8 +56821,8 @@ - - + + @@ -56796,7 +56835,7 @@ - + @@ -56804,7 +56843,7 @@ - + @@ -56815,12 +56854,11 @@ - + - @@ -56830,19 +56868,18 @@ - + - - + - + @@ -56883,8 +56920,6 @@ - - @@ -56894,7 +56929,7 @@ - + @@ -56910,14 +56945,13 @@ - + - + - @@ -56925,7 +56959,7 @@ - + @@ -56939,7 +56973,7 @@ - + @@ -56966,18 +57000,17 @@ - + - + - @@ -56991,7 +57024,7 @@ - + @@ -57015,13 +57048,13 @@ - + @@ -57033,6 +57066,7 @@ + @@ -57063,7 +57097,7 @@ - + @@ -57079,6 +57113,7 @@ + @@ -57098,12 +57133,10 @@ - - @@ -57121,7 +57154,7 @@ - + @@ -57130,6 +57163,7 @@ + @@ -57183,13 +57217,13 @@ + - @@ -57207,15 +57241,15 @@ - + + - @@ -57234,7 +57268,7 @@ - + @@ -57246,6 +57280,7 @@ + @@ -57255,7 +57290,6 @@ - @@ -57265,13 +57299,14 @@ + - + @@ -57296,10 +57331,9 @@ - + - @@ -57309,7 +57343,7 @@ - + @@ -57357,8 +57391,9 @@ + - + @@ -57366,9 +57401,9 @@ - + @@ -57439,13 +57474,14 @@ + - + @@ -57463,7 +57499,6 @@ - @@ -57478,9 +57513,8 @@ - + - @@ -57488,19 +57522,17 @@ - - + + - - @@ -57510,6 +57542,7 @@ + @@ -57521,17 +57554,19 @@ - + - + + + @@ -57540,7 +57575,7 @@ - + @@ -57556,6 +57591,7 @@ + @@ -57578,7 +57614,6 @@ - @@ -57629,17 +57664,17 @@ + - + - + - @@ -57654,9 +57689,12 @@ + + + @@ -57668,6 +57706,7 @@ + @@ -57680,7 +57719,6 @@ - @@ -57703,6 +57741,7 @@ + @@ -57710,7 +57749,6 @@ - @@ -57719,8 +57757,7 @@ - - + @@ -57735,6 +57772,7 @@ + @@ -57753,13 +57791,15 @@ - + + + @@ -57767,7 +57807,7 @@ - + @@ -57788,7 +57828,7 @@ - + @@ -57806,7 +57846,6 @@ - @@ -57816,12 +57855,12 @@ - + - + @@ -57833,7 +57872,7 @@ - + @@ -57844,10 +57883,9 @@ - - + - + @@ -57876,7 +57914,6 @@ - @@ -57895,6 +57932,7 @@ + @@ -57935,6 +57973,7 @@ + @@ -57943,11 +57982,11 @@ - + - + @@ -57958,19 +57997,17 @@ - + - - @@ -58005,10 +58042,10 @@ - + - + @@ -58053,7 +58090,6 @@ - @@ -58061,7 +58097,7 @@ - + @@ -58069,15 +58105,15 @@ - + - + @@ -58097,6 +58133,7 @@ + @@ -58105,10 +58142,10 @@ - + @@ -58123,13 +58160,16 @@ + + + @@ -58185,9 +58225,10 @@ - + + @@ -58198,7 +58239,6 @@ - @@ -58215,18 +58255,19 @@ - + + - + - + @@ -58239,11 +58280,10 @@ - + - @@ -58257,7 +58297,7 @@ - + @@ -58300,7 +58340,7 @@ - + @@ -58310,14 +58350,13 @@ - - + - + @@ -58327,7 +58366,7 @@ - + @@ -58352,7 +58391,6 @@ - @@ -58373,13 +58411,15 @@ + + - + @@ -58392,6 +58432,7 @@ + @@ -58431,16 +58472,16 @@ + - - + @@ -58454,9 +58495,10 @@ - + + @@ -58464,6 +58506,7 @@ + @@ -58506,9 +58549,9 @@ + - @@ -58518,7 +58561,7 @@ - + @@ -58539,7 +58582,6 @@ - @@ -58548,12 +58590,11 @@ - + - @@ -58574,6 +58615,7 @@ + @@ -58589,7 +58631,6 @@ - @@ -58597,13 +58638,14 @@ + - + @@ -58614,12 +58656,12 @@ - + - + @@ -58635,14 +58677,13 @@ - - + @@ -58666,20 +58707,20 @@ - + - + - - + + @@ -58707,7 +58748,6 @@ - @@ -58749,13 +58789,13 @@ + - @@ -58763,7 +58803,7 @@ - + @@ -58793,7 +58833,7 @@ - + @@ -58803,6 +58843,7 @@ + @@ -58818,7 +58859,7 @@ - + @@ -58829,8 +58870,7 @@ - - + @@ -58909,8 +58949,8 @@ + - @@ -58933,6 +58973,7 @@ + @@ -58947,12 +58988,13 @@ - + + @@ -58960,7 +59002,6 @@ - @@ -58972,16 +59013,18 @@ - + + - + + - + @@ -59037,16 +59080,16 @@ - + - + - + @@ -59098,7 +59141,6 @@ - @@ -59126,7 +59168,6 @@ - @@ -59134,7 +59175,7 @@ - + @@ -59142,11 +59183,10 @@ - - - + + @@ -59168,15 +59208,14 @@ - - + @@ -59215,9 +59254,8 @@ - + - @@ -59245,6 +59283,7 @@ + @@ -59259,6 +59298,7 @@ + @@ -59279,6 +59319,7 @@ + @@ -59291,7 +59332,7 @@ - + @@ -59322,7 +59363,6 @@ - @@ -59335,10 +59375,10 @@ - + - + @@ -59355,6 +59395,7 @@ + @@ -59386,9 +59427,8 @@ - + - @@ -59415,6 +59455,8 @@ + + @@ -59426,6 +59468,7 @@ + @@ -59433,7 +59476,6 @@ - @@ -59483,7 +59525,6 @@ - @@ -59493,13 +59534,13 @@ - + - + - + @@ -59556,6 +59597,7 @@ + @@ -59566,6 +59608,7 @@ + @@ -59582,14 +59625,15 @@ - + + - + @@ -59609,7 +59653,6 @@ - @@ -59658,7 +59701,7 @@ - + @@ -59673,9 +59716,10 @@ - + + @@ -59689,14 +59733,12 @@ - + - - @@ -59709,7 +59751,7 @@ - + @@ -59720,8 +59762,9 @@ + - + @@ -59795,16 +59838,14 @@ - + - - @@ -59823,7 +59864,7 @@ - + @@ -59875,7 +59916,6 @@ - @@ -59889,7 +59929,6 @@ - @@ -59900,6 +59939,7 @@ + @@ -59908,7 +59948,7 @@ - + @@ -59938,10 +59978,10 @@ - + @@ -59950,7 +59990,7 @@ - + @@ -59961,16 +60001,16 @@ - + + - @@ -59992,7 +60032,7 @@ - + @@ -60015,11 +60055,10 @@ - + - @@ -60033,7 +60072,7 @@ - + @@ -60060,6 +60099,7 @@ + @@ -60085,7 +60125,7 @@ - + @@ -60104,7 +60144,6 @@ - @@ -60113,7 +60152,6 @@ - @@ -60126,7 +60164,6 @@ - @@ -60144,7 +60181,7 @@ - + @@ -60189,8 +60226,8 @@ - - + + @@ -60213,18 +60250,19 @@ - + + - + - + @@ -60239,7 +60277,6 @@ - @@ -60247,6 +60284,7 @@ + @@ -60280,6 +60318,7 @@ + @@ -60287,7 +60326,6 @@ - @@ -60314,7 +60352,7 @@ - + @@ -60324,7 +60362,6 @@ - @@ -60356,17 +60393,15 @@ - - + - @@ -60379,11 +60414,13 @@ + + @@ -60391,7 +60428,6 @@ - @@ -60417,6 +60453,7 @@ + @@ -60435,7 +60472,7 @@ - + @@ -60476,7 +60513,7 @@ - + @@ -60485,7 +60522,6 @@ - @@ -60499,7 +60535,7 @@ - + @@ -60516,11 +60552,11 @@ - - + + @@ -60539,7 +60575,6 @@ - @@ -60556,8 +60591,8 @@ - - + + @@ -60575,7 +60610,6 @@ - @@ -60587,7 +60621,6 @@ - @@ -60597,15 +60630,14 @@ - + - + - @@ -60621,7 +60653,6 @@ - @@ -60658,7 +60689,7 @@ - + @@ -60671,7 +60702,6 @@ - @@ -60693,29 +60723,28 @@ - - + - - + + @@ -60736,13 +60765,12 @@ - + - - + @@ -60757,7 +60785,6 @@ - @@ -60772,6 +60799,7 @@ + @@ -60791,9 +60819,11 @@ + + @@ -60814,7 +60844,6 @@ - @@ -60822,7 +60851,7 @@ - + @@ -60839,7 +60868,8 @@ - + + @@ -60879,8 +60909,6 @@ - - @@ -60929,6 +60957,7 @@ + @@ -60960,6 +60989,7 @@ + @@ -60998,9 +61028,9 @@ - + @@ -61019,7 +61049,7 @@ - + @@ -61046,15 +61076,15 @@ - - + + - + - + @@ -61065,14 +61095,13 @@ - - + - + @@ -61084,10 +61113,11 @@ + - + @@ -61112,10 +61142,8 @@ - - @@ -61125,8 +61153,8 @@ + - @@ -61136,7 +61164,6 @@ - @@ -61151,7 +61178,7 @@ - + @@ -61159,6 +61186,7 @@ + @@ -61178,9 +61206,10 @@ - + + @@ -61209,6 +61238,7 @@ + @@ -61232,6 +61262,7 @@ + @@ -61253,7 +61284,6 @@ - @@ -61296,7 +61326,7 @@ - + @@ -61318,6 +61348,7 @@ + @@ -61336,12 +61367,11 @@ - + - - + @@ -61354,10 +61384,9 @@ - + - @@ -61376,7 +61405,6 @@ - @@ -61390,7 +61418,6 @@ - @@ -61420,12 +61447,13 @@ + - + @@ -61439,13 +61467,15 @@ - + + + @@ -61461,18 +61491,20 @@ + - + + @@ -61489,6 +61521,7 @@ + @@ -61499,7 +61532,6 @@ - @@ -61513,6 +61545,7 @@ + @@ -61533,12 +61566,11 @@ - + - @@ -61546,7 +61578,7 @@ - + @@ -61567,7 +61599,7 @@ - + @@ -61589,12 +61621,12 @@ + - @@ -61606,7 +61638,7 @@ - + @@ -61629,14 +61661,15 @@ - + + - + @@ -61675,7 +61708,7 @@ - + @@ -61685,9 +61718,8 @@ - - + @@ -61701,7 +61733,7 @@ - + @@ -61724,7 +61756,6 @@ - @@ -61744,14 +61775,12 @@ - - @@ -61772,7 +61801,6 @@ - @@ -61783,13 +61811,13 @@ - + - + @@ -61810,7 +61838,7 @@ - + @@ -61831,10 +61859,11 @@ - + + @@ -61843,12 +61872,12 @@ + - @@ -61879,7 +61908,6 @@ - @@ -61889,6 +61917,7 @@ + @@ -61901,6 +61930,7 @@ + @@ -61910,9 +61940,8 @@ - - + @@ -61924,8 +61953,10 @@ + + @@ -61969,7 +62000,7 @@ - + @@ -61985,6 +62016,7 @@ + @@ -62003,6 +62035,7 @@ + @@ -62015,6 +62048,7 @@ + @@ -62034,17 +62068,17 @@ - + - + @@ -62056,7 +62090,6 @@ - @@ -62069,7 +62102,7 @@ - + @@ -62087,13 +62120,15 @@ + + - + - + @@ -62101,7 +62136,6 @@ - @@ -62119,7 +62153,6 @@ - @@ -62140,6 +62173,7 @@ + @@ -62167,6 +62201,7 @@ + @@ -62176,7 +62211,7 @@ - + @@ -62201,16 +62236,17 @@ - + - + + @@ -62220,6 +62256,7 @@ + @@ -62227,7 +62264,6 @@ - @@ -62243,7 +62279,6 @@ - @@ -62251,8 +62286,10 @@ + + @@ -62267,7 +62304,9 @@ + + @@ -62289,23 +62328,21 @@ - - + - + - - + @@ -62325,7 +62362,6 @@ - @@ -62355,7 +62391,7 @@ - + @@ -62365,7 +62401,7 @@ - + @@ -62378,14 +62414,13 @@ - + - + - @@ -62398,7 +62433,6 @@ - @@ -62426,7 +62460,6 @@ - @@ -62460,26 +62493,25 @@ - + - + - - + @@ -62505,7 +62537,6 @@ - @@ -62517,6 +62548,7 @@ + @@ -62533,6 +62565,7 @@ + @@ -62545,7 +62578,6 @@ - @@ -62581,9 +62613,9 @@ - + @@ -62640,6 +62672,7 @@ + @@ -62648,11 +62681,12 @@ + - + @@ -62712,7 +62746,7 @@ - + @@ -62731,12 +62765,12 @@ - - + + @@ -62747,16 +62781,15 @@ - - + - + @@ -62767,12 +62800,11 @@ - - + @@ -62783,13 +62815,13 @@ - + - + - + @@ -62804,13 +62836,14 @@ - + - + + @@ -62852,6 +62885,7 @@ + @@ -62878,6 +62912,7 @@ + @@ -62885,6 +62920,7 @@ + @@ -62908,6 +62944,7 @@ + @@ -62917,7 +62954,6 @@ - @@ -62948,7 +62984,6 @@ - @@ -62959,10 +62994,10 @@ - + - + @@ -62975,6 +63010,7 @@ + @@ -62988,6 +63024,7 @@ + @@ -63003,10 +63040,9 @@ - - + @@ -63015,7 +63051,6 @@ - @@ -63034,13 +63069,13 @@ - - + + @@ -63063,11 +63098,9 @@ - - @@ -63080,6 +63113,7 @@ + @@ -63095,7 +63129,7 @@ - + @@ -63106,7 +63140,6 @@ - @@ -63116,8 +63149,7 @@ - - + @@ -63133,7 +63165,6 @@ - @@ -63157,6 +63188,7 @@ + @@ -63170,6 +63202,7 @@ + @@ -63213,7 +63246,6 @@ - @@ -63226,7 +63258,6 @@ - @@ -63235,7 +63266,6 @@ - @@ -63247,7 +63277,7 @@ - + @@ -63255,8 +63285,9 @@ - + + @@ -63278,7 +63309,7 @@ - + @@ -63293,7 +63324,6 @@ - @@ -63311,12 +63341,12 @@ + - @@ -63338,7 +63368,7 @@ - + @@ -63354,6 +63384,7 @@ + @@ -63361,8 +63392,8 @@ - - + + @@ -63401,7 +63432,6 @@ - @@ -63420,6 +63450,7 @@ + @@ -63436,6 +63467,7 @@ + @@ -63445,6 +63477,7 @@ + @@ -63461,10 +63494,9 @@ - - - + + @@ -63479,6 +63511,7 @@ + @@ -63514,6 +63547,7 @@ + @@ -63533,7 +63567,6 @@ - @@ -63543,6 +63576,7 @@ + @@ -63555,13 +63589,14 @@ + - + - + @@ -63570,7 +63605,7 @@ - + @@ -63579,7 +63614,7 @@ - + @@ -63590,7 +63625,7 @@ - + @@ -63602,7 +63637,6 @@ - @@ -63620,7 +63654,7 @@ - + @@ -63648,6 +63682,7 @@ + @@ -63662,7 +63697,7 @@ - + @@ -63674,9 +63709,9 @@ - + - + @@ -63688,8 +63723,9 @@ - + + @@ -63700,7 +63736,7 @@ - + @@ -63718,7 +63754,7 @@ - + @@ -63733,8 +63769,8 @@ - + @@ -63744,10 +63780,9 @@ - + - @@ -63772,7 +63807,6 @@ - @@ -63788,7 +63822,7 @@ - + @@ -63814,14 +63848,14 @@ - + - + - + @@ -63849,10 +63883,10 @@ + - @@ -63877,7 +63911,6 @@ - @@ -63902,13 +63935,13 @@ + - @@ -63947,6 +63980,7 @@ + @@ -63955,10 +63989,11 @@ + - + @@ -63987,10 +64022,10 @@ + - @@ -63998,10 +64033,10 @@ - + - + @@ -64012,7 +64047,6 @@ - @@ -64029,7 +64063,8 @@ - + + @@ -64042,10 +64077,11 @@ - + + @@ -64070,19 +64106,19 @@ - + - + @@ -64090,14 +64126,12 @@ - - - + @@ -64106,11 +64140,11 @@ - - + + @@ -64128,8 +64162,9 @@ - + + @@ -64155,13 +64190,13 @@ - + - + @@ -64190,12 +64225,13 @@ + - + @@ -64203,11 +64239,12 @@ + - + @@ -64227,7 +64264,7 @@ - + @@ -64247,7 +64284,6 @@ - @@ -64255,7 +64291,6 @@ - @@ -64267,6 +64302,7 @@ + @@ -64280,7 +64316,8 @@ - + + @@ -64288,6 +64325,7 @@ + @@ -64302,7 +64340,6 @@ - @@ -64328,7 +64365,7 @@ - + @@ -64342,7 +64379,8 @@ - + + @@ -64355,7 +64393,8 @@ - + + @@ -64365,6 +64404,7 @@ + @@ -64395,16 +64435,16 @@ + - + - @@ -64449,11 +64489,11 @@ + - @@ -64468,10 +64508,11 @@ + - - + + @@ -64480,7 +64521,7 @@ - + @@ -64513,9 +64554,10 @@ - + + @@ -64532,14 +64574,15 @@ - + + - + @@ -64556,7 +64599,7 @@ - + @@ -64566,6 +64609,7 @@ + @@ -64590,7 +64634,6 @@ - @@ -64613,6 +64656,7 @@ + @@ -64620,8 +64664,7 @@ - - + @@ -64641,7 +64684,6 @@ - @@ -64657,11 +64699,10 @@ - - + @@ -64670,9 +64711,9 @@ + - @@ -64697,7 +64738,6 @@ - @@ -64705,12 +64745,12 @@ + - @@ -64724,7 +64764,7 @@ - + @@ -64739,7 +64779,7 @@ - + @@ -64817,11 +64857,10 @@ - + - @@ -64829,7 +64868,7 @@ - + @@ -64837,7 +64876,6 @@ - @@ -64849,9 +64887,10 @@ + - + @@ -64874,7 +64913,7 @@ - + @@ -64891,7 +64930,7 @@ - + @@ -64904,7 +64943,6 @@ - @@ -64923,12 +64961,10 @@ - - - + @@ -64954,6 +64990,7 @@ + @@ -64965,10 +65002,9 @@ - + - @@ -64981,17 +65017,19 @@ + + - + @@ -65000,12 +65038,11 @@ - - + @@ -65031,7 +65068,7 @@ - + @@ -65060,10 +65097,12 @@ + - + + @@ -65074,7 +65113,6 @@ - @@ -65084,7 +65122,8 @@ - + + @@ -65105,13 +65144,10 @@ - - - @@ -65121,14 +65157,15 @@ - + + @@ -65147,6 +65184,7 @@ + @@ -65166,23 +65204,23 @@ - + + - + - @@ -65192,17 +65230,18 @@ - + + + - @@ -65214,7 +65253,7 @@ - + @@ -65235,7 +65274,6 @@ - @@ -65245,19 +65283,19 @@ - + - + - + @@ -65278,7 +65316,7 @@ - + @@ -65289,7 +65327,8 @@ - + + @@ -65307,7 +65346,7 @@ - + @@ -65316,21 +65355,21 @@ - - + + - + @@ -65351,10 +65390,9 @@ - + - @@ -65379,11 +65417,10 @@ - - + @@ -65406,7 +65443,7 @@ - + @@ -65420,6 +65457,7 @@ + @@ -65441,7 +65479,7 @@ - + @@ -65461,7 +65499,7 @@ - + @@ -65472,6 +65510,7 @@ + @@ -65500,8 +65539,9 @@ + - + @@ -65519,7 +65559,7 @@ - + @@ -65565,7 +65605,7 @@ - + @@ -65581,6 +65621,7 @@ + @@ -65599,18 +65640,20 @@ - + + - + + @@ -65625,7 +65668,6 @@ - @@ -65636,9 +65678,10 @@ - + + @@ -65649,6 +65692,7 @@ + @@ -65656,23 +65700,21 @@ - + - + - - @@ -65689,13 +65731,8 @@ - - - - - @@ -65708,16 +65745,20 @@ - + + + + + @@ -65729,7 +65770,6 @@ - @@ -65743,7 +65783,7 @@ - + @@ -65759,23 +65799,23 @@ - + + - - + @@ -65794,25 +65834,27 @@ + - + + + - - + - + @@ -65823,55 +65865,55 @@ - + + - - + - + + + + - - + - + - - - - + + @@ -65884,11 +65926,16 @@ + + - + + + + @@ -65897,11 +65944,12 @@ - + + @@ -65910,21 +65958,22 @@ - + - + + @@ -65933,6 +65982,7 @@ + @@ -65943,7 +65993,6 @@ - @@ -65957,144 +66006,148 @@ - + + + - - + + - - + - + - - - + - + - - - + + + - + - + - - - - + - - - + + + - + - + + + + + + + + - - + + + @@ -66103,40 +66156,37 @@ - + - - + + + + - + - - - - - @@ -66147,18 +66197,16 @@ - - + - - + @@ -66167,12 +66215,13 @@ - + + - + @@ -66181,41 +66230,41 @@ - + + - - - + - + + @@ -66228,40 +66277,44 @@ + + + - + + + - + - - + @@ -66270,7 +66323,6 @@ - @@ -66278,17 +66330,14 @@ - - - - + @@ -66296,40 +66345,39 @@ + + - + + - - - + + + - - - - @@ -66337,26 +66385,29 @@ + + - + + - + - + @@ -66365,16 +66416,18 @@ + - + - + + @@ -66382,7 +66435,6 @@ - @@ -66398,21 +66450,29 @@ + + + + + + + + - + @@ -66429,30 +66489,33 @@ + - - + + + + - + @@ -66461,62 +66524,61 @@ - + - - + + + - - - + + + - - + - - - + + - + @@ -66525,32 +66587,28 @@ - + - - - - + + - - @@ -66560,9 +66618,11 @@ + + - + @@ -66578,20 +66638,22 @@ - - + + + + + - @@ -66600,30 +66662,29 @@ - - + - - + + + + - - @@ -66638,13 +66699,11 @@ - - @@ -66652,6 +66711,7 @@ + @@ -66661,6 +66721,7 @@ + @@ -66668,7 +66729,6 @@ - @@ -66682,21 +66742,24 @@ - + + + + + - @@ -66707,30 +66770,31 @@ - + + - - + - - + + + @@ -66738,27 +66802,24 @@ + - - + - + - - - @@ -66767,62 +66828,55 @@ - + - - + - - + + - + - - - - - + - - @@ -66835,25 +66889,23 @@ + - - + - - @@ -66866,15 +66918,12 @@ - - - - + @@ -66883,25 +66932,21 @@ - - - + - - + - @@ -66916,6 +66961,9 @@ + + + @@ -66931,30 +66979,34 @@ + - + + + - + + + - - + - + @@ -66962,13 +67014,11 @@ - - - + @@ -66985,11 +67035,11 @@ + - @@ -67000,15 +67050,13 @@ - - + - @@ -67016,22 +67064,26 @@ - - + + - + + + + + @@ -67039,22 +67091,17 @@ - - + + - - - - - @@ -67064,15 +67111,15 @@ - + - + @@ -67080,38 +67127,31 @@ + - + - + - - - - - - - - @@ -67119,16 +67159,15 @@ - - + @@ -67139,67 +67178,61 @@ - - - + + + - - - + - + - - - - - + + - - + @@ -67207,17 +67240,15 @@ - - - - + + @@ -67234,47 +67265,48 @@ + - + - + + + - + - - - + + - + - - + + - @@ -67286,7 +67318,6 @@ - @@ -67294,60 +67325,67 @@ + + + + - + + + + + - - - - - + + - + + + @@ -67355,10 +67393,12 @@ + + @@ -67367,17 +67407,15 @@ + - + - - - @@ -67390,46 +67428,48 @@ + + + + - - - + + + - - - + @@ -67440,17 +67480,18 @@ + - + + - @@ -67459,14 +67500,14 @@ + + - - @@ -67490,14 +67531,12 @@ - - @@ -67511,17 +67550,14 @@ + - - - - @@ -67529,31 +67565,28 @@ - - - + - + + + - - + - - @@ -67561,21 +67594,18 @@ - - - + + + - - + - - + - - + @@ -67585,6 +67615,8 @@ + + @@ -67593,8 +67625,10 @@ + + @@ -67604,6 +67638,7 @@ + @@ -67614,7 +67649,6 @@ - @@ -67622,48 +67656,49 @@ - + - + - - + - + - + + + - + @@ -67673,14 +67708,14 @@ - + + - @@ -67689,17 +67724,18 @@ + - + - + @@ -67707,30 +67743,35 @@ - + + - + + + + + - + - + @@ -67741,7 +67782,6 @@ - @@ -67751,11 +67791,10 @@ + + - - - @@ -67768,6 +67807,7 @@ + @@ -67775,17 +67815,17 @@ - - + + - + - + @@ -67793,34 +67833,33 @@ + - - - - + + + - - + + - - + @@ -67832,14 +67871,14 @@ + - - + @@ -67849,53 +67888,48 @@ + - - - + - - + + + + - - + - + - - - - - @@ -67905,36 +67939,36 @@ - - + - + + + - + - + - @@ -67945,11 +67979,13 @@ + + @@ -67958,10 +67994,8 @@ - - @@ -67969,39 +68003,45 @@ - + + - - + - - + + + + - + - + + + + + + - @@ -68012,15 +68052,14 @@ + - - @@ -68028,12 +68067,9 @@ - - - @@ -68046,80 +68082,77 @@ - + - - + - + + + - - + + + - + + - + - - - - - - + - - + @@ -68129,31 +68162,27 @@ - + - - - - + - @@ -68166,23 +68195,23 @@ + + + - - + - - @@ -68190,16 +68219,19 @@ + + + + - - + @@ -68211,33 +68243,22 @@ - + - + - - - - - - - - - - - @@ -68245,14 +68266,15 @@ + + + - - @@ -68265,9 +68287,13 @@ + + + + @@ -68278,55 +68304,60 @@ + + - + - - - + + + - + + + + + - - + - + @@ -68336,44 +68367,41 @@ - - - + + - + + + - - - - - + @@ -68384,12 +68412,14 @@ - + + + @@ -68403,21 +68433,24 @@ - + + + - + + @@ -68427,16 +68460,19 @@ + + + @@ -68445,13 +68481,15 @@ - + + + + - @@ -68459,17 +68497,22 @@ - + + + - + + + + @@ -68480,44 +68523,41 @@ - - + + + - + + - + - - - - + + - - - @@ -68529,19 +68569,20 @@ - - + + + diff --git a/Engine/Config/BaseDeviceProfiles.ini b/Engine/Config/BaseDeviceProfiles.ini index b351f2e65d30..08a82a6bc9be 100644 --- a/Engine/Config/BaseDeviceProfiles.ini +++ b/Engine/Config/BaseDeviceProfiles.ini @@ -422,7 +422,7 @@ BaseProfileName= +CVars=r.RefractionQuality=0 +CVars=r.ShadowQuality=2 +CVars=slate.AbsoluteIndices=1 -+CVars=r.Vulkan.DelayAcquireBackBuffer=1 ++CVars=r.Vulkan.DelayAcquireBackBuffer=0 +CVars=r.Vulkan.RobustBufferAccess=1 +CVars=r.Vulkan.DescriptorSetLayoutMode=2 diff --git a/Engine/Config/BaseEngine.ini b/Engine/Config/BaseEngine.ini index 52153d86b568..458fe5219f64 100644 --- a/Engine/Config/BaseEngine.ini +++ b/Engine/Config/BaseEngine.ini @@ -631,7 +631,8 @@ bUseStreamingPause=false +ClassRedirects=(OldName="LevelSequenceDirectorGeneratedClass",NewName="/Script/Engine.BlueprintGeneratedClass") ; 4.22 -+FunctionRedirects=(OldName="UserWidget.PlayAnimation",NewName="UserWidget.PlayAnimationAtTime") ++FunctionRedirects=(OldName="UserWidget.PlayAnimationTo",NewName="UserWidget.PlayAnimationTimeRange") ++FunctionRedirects=(OldName="UserWidget.PlayAnimationAtTime",NewName="UserWidget.PlayAnimation") +FunctionRedirects=(OldName="AddChildWrapBox", NewName="AddChildToWrapBox") ; Chaos @@ -903,6 +904,9 @@ CooloffTime=10 NetConnectionClassName="/Script/Engine.DemoNetConnection" DemoSpectatorClass=Engine.PlayerController SpawnPrioritySeconds=60.0 +!ChannelDefinitions=CLEAR_ARRAY ++ChannelDefinitions=(ChannelName=Control, ClassName=/Script/Engine.ControlChannel, StaticChannelIndex=0, bTickOnCreate=true, bServerOpen=false, bClientOpen=true, bInitialServer=false, bInitialClient=true) ++ChannelDefinitions=(ChannelName=Actor, ClassName=/Script/Engine.ActorChannel, StaticChannelIndex=-1, bTickOnCreate=false, bServerOpen=true, bClientOpen=false, bInitialServer=false, bInitialClient=false) [TextureStreaming] NeverStreamOutTextures=False @@ -1797,6 +1801,7 @@ DebugInfoStartY=60.0 [/Script/IOSRuntimeSettings.IOSRuntimeSettings] bEnableGameCenterSupport=False bSupportsPortraitOrientation=False +bSupportsITunesFileSharing=False bSupportsUpsideDownOrientation=False bSupportsLandscapeLeftOrientation=True bSupportsLandscapeRightOrientation=True @@ -2180,4 +2185,4 @@ LiveLinkPublishingPort=11111 r.GPUCrashDebugging=false [Messaging] -bAllowDelayedMessaging=false \ No newline at end of file +bAllowDelayedMessaging=false diff --git a/Engine/Config/BasePakFileRules.ini b/Engine/Config/BasePakFileRules.ini new file mode 100644 index 000000000000..b86b72085df8 --- /dev/null +++ b/Engine/Config/BasePakFileRules.ini @@ -0,0 +1,18 @@ +; These rules are applied in order, the first rule that applies per file is taken and no others are evaluated +; [SectionName] +; bOverrideChunkManifest=false ; If true this allows overriding assignments from the cooker +; bExcludeFromPaks=false ; If true this removes entirely, cannot coexist with overridepaks +; OverridePaks="pakchunk1" ; If set this will override pak list, comma separated +; Platforms="iOS,Android" ; If set this rule will only apply to these platforms +; Targets="Shipping,Test" ; If set this rule will only apply to these configurations +; +Files=".../*FileMask*.*" ; List of file masks to apply to, using the C# FileFilter class + + +[ExcludeContentForMobile] +; Exclude specific large textures on mobile platforms, this was moved from CopyBuildToStagingDirectory.cs +; This can be added to in a game's DefaultPakFileRules.ini by using the same sections, and new sections can be added +; To remove this rule, use !Files to clear the list of file paths +Platforms="Android,iOS,tvOS,HTML5" +bExcludeFromPaks=true +bOverrideChunkManifest=true ++Files=".../Engine/Content/EngineMaterials/DefaultBloomKernel.*" diff --git a/Engine/Extras/LLDBDataFormatters/UE4DataFormatters.py b/Engine/Extras/LLDBDataFormatters/UE4DataFormatters.py index c2bc0fb20fa0..e0c6d182c7b5 100644 --- a/Engine/Extras/LLDBDataFormatters/UE4DataFormatters.py +++ b/Engine/Extras/LLDBDataFormatters/UE4DataFormatters.py @@ -59,7 +59,7 @@ def UE4FNameSummaryProvider(valobj,dict): if not Index.IsValid(): Index = valobj.GetChildMemberWithName('ComparisonIndex') IndexVal = Index.GetValueAsUnsigned(0) - if IndexVal >= 1048576: + if IndexVal >= 4194304: return 'name=Invalid' else: Expr = '(char*)(((FNameEntry***)GFNameTableForDebuggerVisualizers_MT)['+str(IndexVal)+' / 16384]['+str(IndexVal)+' % 16384])->AnsiName' @@ -74,7 +74,7 @@ def UE4FNameSummaryProvider(valobj,dict): def UE4FMinimalNameSummaryProvider(valobj,dict): Index = valobj.GetChildMemberWithName('Index') IndexVal = Index.GetValueAsUnsigned(0) - if IndexVal >= 1048576: + if IndexVal >= 4194304: return 'name=Invalid' else: Expr = '(char*)(((FNameEntry***)GFNameTableForDebuggerVisualizers_MT)['+str(IndexVal)+' / 16384]['+str(IndexVal)+' % 16384])->AnsiName' diff --git a/Engine/Extras/LLDBDataFormatters/UE4DataFormatters_2ByteChars.py b/Engine/Extras/LLDBDataFormatters/UE4DataFormatters_2ByteChars.py index bb6ea7ed6a43..44a353cbeff9 100644 --- a/Engine/Extras/LLDBDataFormatters/UE4DataFormatters_2ByteChars.py +++ b/Engine/Extras/LLDBDataFormatters/UE4DataFormatters_2ByteChars.py @@ -59,7 +59,7 @@ def UE4FNameSummaryProvider(valobj,dict): if not Index.IsValid(): Index = valobj.GetChildMemberWithName('ComparisonIndex') IndexVal = Index.GetValueAsUnsigned(0) - if IndexVal >= 1048576: + if IndexVal >= 4194304: return 'name=Invalid' else: Expr = '(char*)(((FNameEntry***)FName::GetNameTableForDebuggerVisualizers_MT())['+str(IndexVal)+' / 16384]['+str(IndexVal)+' % 16384])->AnsiName' @@ -74,7 +74,7 @@ def UE4FNameSummaryProvider(valobj,dict): def UE4FMinimalNameSummaryProvider(valobj,dict): Index = valobj.GetChildMemberWithName('Index') IndexVal = Index.GetValueAsUnsigned(0) - if IndexVal >= 1048576: + if IndexVal >= 4194304: return 'name=Invalid' else: Expr = '(char*)(((FNameEntry***)FName::GetNameTableForDebuggerVisualizers_MT())['+str(IndexVal)+' / 16384]['+str(IndexVal)+' % 16384])->AnsiName' diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/LICENSE b/Engine/Extras/ThirdPartyNotUE/ios-deploy/LICENSE new file mode 100644 index 000000000000..f8e017ae89b9 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/LICENSE @@ -0,0 +1,8 @@ +ios-deploy is available under the provisions of the GNU General Public License, +version 3 (or later), available here: http://www.gnu.org/licenses/gpl-3.0.html + + +Error codes used for error messages were taken from SDMMobileDevice framework, +originally reverse engineered by Sam Marshall. SDMMobileDevice is distributed +under BSD 3-Clause license and is available here: +https://github.com/samdmarshall/SDMMobileDevice diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/README.md b/Engine/Extras/ThirdPartyNotUE/ios-deploy/README.md new file mode 100644 index 000000000000..a5d4f2b41f8b --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/README.md @@ -0,0 +1,142 @@ +[![Build Status](https://travis-ci.org/phonegap/ios-deploy.svg?branch=master)](https://travis-ci.org/phonegap/ios-deploy) + +ios-deploy +========== +Install and debug iOS apps from the command line. Designed to work on un-jailbroken devices. + +## Requirements + +* Mac OS X. Tested on 10.11 El Capitan, 10.12 Sierra, iOS 9.0 and iOS 10.0 +* You need to have a valid iOS Development certificate installed. +* Xcode 7 or greater should be installed (**NOT** Command Line Tools!) + +## Roadmap + +See our [milestones](https://github.com/phonegap/ios-deploy/milestones). + +## Development + +The 1.x branch has been archived (renamed for now), all development is to be on the master branch for simplicity, since the planned 2.x development (break out commands into their own files) has been abandoned for now. + +## Installation +======= + +ios-deploy installation is made simple using the node.js package manager. If you use [Homebrew](http://brew.sh/), install [node.js](https://nodejs.org): + +``` +brew install node +``` + +Now install ios-deploy with the [node.js](https://nodejs.org) package manager: + +``` +npm install -g ios-deploy +``` + +To build from source: + +``` +xcodebuild +``` + +This will build `ios-deploy` into the `build/Release` folder. + +## Testing + +Run: + +``` +npm install && npm test +``` + +### OS X 10.11 El Capitan or greater + +If you are *not* using a node version manager like [nvm](https://github.com/creationix/nvm) or [n](https://github.com/tj/n), you may have to do either of these three things below when under El Capitan: + +1. Add the `--unsafe-perm=true` flag when installing ios-deploy +2. Add the `--allow-root` flag when installing ios-deploy +3. Ensure the `nobody` user has write access to `/usr/local/lib/node_modules/ios-deploy/ios-deploy` + +## Usage + + Usage: ios-deploy [OPTION]... + -d, --debug launch the app in lldb after installation + -i, --id the id of the device to connect to + -c, --detect only detect if the device is connected + -b, --bundle the path to the app bundle to be installed + -a, --args command line arguments to pass to the app when launching it + -t, --timeout number of seconds to wait for a device to be connected + -u, --unbuffered don't buffer stdout + -n, --nostart do not start the app when debugging + -I, --noninteractive start in non interactive mode (quit when app crashes or exits) + -L, --justlaunch just launch the app and exit lldb + -v, --verbose enable verbose output + -m, --noinstall directly start debugging without app install (-d not required) + -p, --port port used for device, default: dynamic + -r, --uninstall uninstall the app before install (do not use with -m; app cache and data are cleared) + -9, --uninstall_only uninstall the app ONLY. Use only with -1 + -1, --bundle_id specify bundle id for list and upload + -l, --list list files + -o, --upload upload file + -w, --download download app tree + -2, --to use together with up/download file/tree. specify target + -D, --mkdir make directory on device + -R, --rm remove file or directory on device (directories must be empty) + -V, --version print the executable version + -e, --exists check if the app with given bundle_id is installed or not + -B, --list_bundle_id list bundle_id + -W, --no-wifi ignore wifi devices + --detect_deadlocks start printing backtraces for all threads periodically after specific amount of seconds + +## Examples + +The commands below assume that you have an app called `my.app` with bundle id `bundle.id`. Substitute where necessary. + + // deploy and debug your app to a connected device + ios-deploy --debug --bundle my.app + + // deploy and debug your app to a connected device, skipping any wi-fi connection (use USB) + ios-deploy --debug --bundle my.app --no-wifi + + // deploy and launch your app to a connected device, but quit the debugger after + ios-deploy --justlaunch --debug --bundle my.app + + // deploy and launch your app to a connected device, quit when app crashes or exits + ios-deploy --noninteractive --debug --bundle my.app + + // Upload a file to your app's Documents folder + ios-deploy --bundle_id 'bundle.id' --upload test.txt --to Documents/test.txt + + // Download your app's Documents, Library and tmp folders + ios-deploy --bundle_id 'bundle.id' --download --to MyDestinationFolder + + // List the contents of your app's Documents, Library and tmp folders + ios-deploy --bundle_id 'bundle.id' --list + + // deploy and debug your app to a connected device, uninstall the app first + ios-deploy --uninstall --debug --bundle my.app + + // check whether an app by bundle id exists on the device (check return code `echo $?`) + ios-deploy --exists --bundle_id com.apple.mobilemail + + // Download the Documents directory of the app *only* + ios-deploy --download=/Documents --bundle_id my.app.id --to ./my_download_location + + // List ids and names of connected devices + ios-deploy -c + + // Uninstall an app + ios-deploy --uninstall_only --bundle_id my.bundle.id + + // list all bundle ids of all apps on your device + ios-deploy --list_bundle_id + +## Demo + +The included demo.app represents the minimum required to get code running on iOS. + +* `make demo.app` will generate the demo.app executable. If it doesn't compile, modify `IOS_SDK_VERSION` in the Makefile. +* `make debug` will install demo.app and launch a LLDB session. + +## Notes +* `--detect_deadlocks` can help to identify an exact state of application's threads in case of a deadlock. It works like this: The user specifies the amount of time ios-deploy runs the app as usual. When the timeout is elapsed ios-deploy starts to print call-stacks of all threads every 5 seconds and the app keeps running. Comparing threads' call-stacks between each other helps to identify the threads which were stuck. diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/.gitignore b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/.gitignore new file mode 100644 index 000000000000..c2ef55db28e2 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/.gitignore @@ -0,0 +1,5 @@ +demo +demo.app +demo.dSYM +/.DS_Store +*~ diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/Makefile b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/Makefile new file mode 100644 index 000000000000..319e9f8878ce --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/Makefile @@ -0,0 +1,24 @@ +IOS_SDK_VERSION = 9.1 + +IOS_CC = gcc -ObjC +IOS_SDK = $(shell xcode-select --print-path)/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS$(IOS_SDK_VERSION).sdk + +all: clean demo.app + +demo.app: demo Info.plist + mkdir -p demo.app + cp demo demo.app/ + cp Info.plist ResourceRules.plist demo.app/ + codesign -f -s "iPhone Developer" --entitlements Entitlements.plist demo.app + +demo: demo.c + $(IOS_CC) -g -arch armv7 -isysroot $(IOS_SDK) -framework CoreFoundation -o demo demo.c + +debug: all ios-deploy + @../build/Release/ios-deploy --debug --bundle demo.app + +clean: + @rm -rf *.app demo demo.dSYM + +ios-deploy: + @xcodebuild -project ../ios-deploy.xcodeproj diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/demo.c b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/demo.c new file mode 100644 index 000000000000..b366b145ab3b --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/demo/demo.c @@ -0,0 +1,9 @@ +#include + +int main(int argc, const char* argv[]) { + int i; + for (i = 0; i < argc; i++) { + printf("argv[%d] = %s\n", i, argv[i]); + } + return 0; +} \ No newline at end of file diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy.tps b/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy.tps new file mode 100644 index 000000000000..031d694d1fe6 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy.tps @@ -0,0 +1,11 @@ + + + ios-deploy + Engine\Extras\ThirdPartyNotUE\ios-deploy + Used by Gauntlet automation to deploy and launch apps on iOS devices. + https://github.com/ios-control/ios-deploy/blob/master/LICENSE + + P4 + + Engine\Extras\ThirdPartyNotUE\ios-deploy\ios-deploy_License.txt + \ No newline at end of file diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy_License.txt b/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy_License.txt new file mode 100644 index 000000000000..cf2d0fdab1ec --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/ios-deploy_License.txt @@ -0,0 +1,10 @@ +This work was modified by Epic Games, Inc., 2018. + +ios-deploy is available under the provisions of the GNU General Public License, +version 3 (or later), available here: http://www.gnu.org/licenses/gpl-3.0.html + + +Error codes used for error messages were taken from SDMMobileDevice framework, +originally reverse engineered by Sam Marshall. SDMMobileDevice is distributed +under BSD 3-Clause license and is available here: +https://github.com/samdmarshall/SDMMobileDevice \ No newline at end of file diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/package.json b/Engine/Extras/ThirdPartyNotUE/ios-deploy/package.json new file mode 100644 index 000000000000..f3d7b7c79e5e --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/package.json @@ -0,0 +1,39 @@ +{ + "name": "ios-deploy", + "version": "1.9.4", + "os": [ + "darwin" + ], + "description": "launch iOS apps iOS devices from the command line (Xcode 7)", + "main": "ios-deploy", + "bin": "./build/Release/ios-deploy", + "repository": { + "type": "git", + "url": "https://github.com/ios-control/ios-deploy" + }, + "devDependencies": { + "eslint": "~4.19.1", + "eslint-config-semistandard": "^12.0.1", + "eslint-config-standard": "^11.0.0", + "eslint-plugin-import": "^2.12.0", + "eslint-plugin-node": "^6.0.1", + "eslint-plugin-promise": "^3.8.0", + "eslint-plugin-standard": "^3.1.0" + }, + "scripts": { + "preinstall": "./src/scripts/check_reqs.js && xcodebuild", + "build-test": "npm run pycompile && xcodebuild -target ios-deploy-lib && xcodebuild test -scheme ios-deploy-tests", + "eslint": "eslint src/scripts/*.js", + "test": "npm run eslint && npm run build-test", + "pycompile": "python -m py_compile src/scripts/*.py" + }, + "keywords": [ + "ios-deploy", + "deploy to iOS device" + ], + "bugs": { + "url": "https://github.com/phonegap/ios-deploy/issues" + }, + "author": "Greg Hughes", + "license": "GPLv3" +} diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.h b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.h new file mode 100644 index 000000000000..78323bc9bd2f --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.h @@ -0,0 +1,5 @@ +#import + +@interface ios_deploy_lib : NSObject + +@end diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.m b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.m new file mode 100644 index 000000000000..5993a9b5d39b --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-lib/libios_deploy.m @@ -0,0 +1,5 @@ +#import "libios_deploy.h" + +@implementation ios_deploy_lib + +@end diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-tests/ios_deploy_tests.m b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-tests/ios_deploy_tests.m new file mode 100644 index 000000000000..f58e94935299 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy-tests/ios_deploy_tests.m @@ -0,0 +1,31 @@ +#import + +@interface ios_deploy_tests : XCTestCase + +@end + +@implementation ios_deploy_tests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. +} + +- (void)testPerformanceExample { + // This is an example of a performance test case. + [self measureBlock:^{ + // Put the code you want to measure the time of here. + }]; +} + +@end diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/MobileDevice.h b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/MobileDevice.h new file mode 100644 index 000000000000..89b7d5aaff1c --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/MobileDevice.h @@ -0,0 +1,497 @@ +/* ---------------------------------------------------------------------------- + * MobileDevice.h - interface to MobileDevice.framework + * $LastChangedDate: 2007-07-09 18:59:29 -0700 (Mon, 09 Jul 2007) $ + * + * Copied from http://iphonesvn.halifrag.com/svn/iPhone/ + * With modifications from Allen Porter and Scott Turner + * + * ------------------------------------------------------------------------- */ + +#ifndef MOBILEDEVICE_H +#define MOBILEDEVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(WIN32) +#include +typedef unsigned int mach_error_t; +#elif defined(__APPLE__) +#include +#include +#endif + +/* Error codes */ +#define MDERR_APPLE_MOBILE (err_system(0x3a)) +#define MDERR_IPHONE (err_sub(0)) + +/* Apple Mobile (AM*) errors */ +#define MDERR_OK ERR_SUCCESS +#define MDERR_SYSCALL (ERR_MOBILE_DEVICE | 0x01) +#define MDERR_OUT_OF_MEMORY (ERR_MOBILE_DEVICE | 0x03) +#define MDERR_QUERY_FAILED (ERR_MOBILE_DEVICE | 0x04) +#define MDERR_INVALID_ARGUMENT (ERR_MOBILE_DEVICE | 0x0b) +#define MDERR_DICT_NOT_LOADED (ERR_MOBILE_DEVICE | 0x25) + +/* Apple File Connection (AFC*) errors */ +#define MDERR_AFC_OUT_OF_MEMORY 0x03 + +/* USBMux errors */ +#define MDERR_USBMUX_ARG_NULL 0x16 +#define MDERR_USBMUX_FAILED 0xffffffff + +/* Messages passed to device notification callbacks: passed as part of + * am_device_notification_callback_info. */ +#define ADNCI_MSG_CONNECTED 1 +#define ADNCI_MSG_DISCONNECTED 2 +#define ADNCI_MSG_UNKNOWN 3 + +#define AMD_IPHONE_PRODUCT_ID 0x1290 +#define AMD_IPHONE_SERIAL "3391002d9c804d105e2c8c7d94fc35b6f3d214a3" + +/* Services, found in /System/Library/Lockdown/Services.plist */ +#define AMSVC_AFC CFSTR("com.apple.afc") +#define AMSVC_BACKUP CFSTR("com.apple.mobilebackup") +#define AMSVC_CRASH_REPORT_COPY CFSTR("com.apple.crashreportcopy") +#define AMSVC_DEBUG_IMAGE_MOUNT CFSTR("com.apple.mobile.debug_image_mount") +#define AMSVC_NOTIFICATION_PROXY CFSTR("com.apple.mobile.notification_proxy") +#define AMSVC_PURPLE_TEST CFSTR("com.apple.purpletestr") +#define AMSVC_SOFTWARE_UPDATE CFSTR("com.apple.mobile.software_update") +#define AMSVC_SYNC CFSTR("com.apple.mobilesync") +#define AMSVC_SCREENSHOT CFSTR("com.apple.screenshotr") +#define AMSVC_SYSLOG_RELAY CFSTR("com.apple.syslog_relay") +#define AMSVC_SYSTEM_PROFILER CFSTR("com.apple.mobile.system_profiler") + +typedef unsigned int afc_error_t; +typedef unsigned int usbmux_error_t; +typedef unsigned int service_conn_t; + +struct am_recovery_device; + +typedef struct am_device_notification_callback_info { + struct am_device *dev; /* 0 device */ + unsigned int msg; /* 4 one of ADNCI_MSG_* */ +} __attribute__ ((packed)) am_device_notification_callback_info; + +/* The type of the device restore notification callback functions. + * TODO: change to correct type. */ +typedef void (*am_restore_device_notification_callback)(struct + am_recovery_device *); + +/* This is a CoreFoundation object of class AMRecoveryModeDevice. */ +typedef struct am_recovery_device { + unsigned char unknown0[8]; /* 0 */ + am_restore_device_notification_callback callback; /* 8 */ + void *user_info; /* 12 */ + unsigned char unknown1[12]; /* 16 */ + unsigned int readwrite_pipe; /* 28 */ + unsigned char read_pipe; /* 32 */ + unsigned char write_ctrl_pipe; /* 33 */ + unsigned char read_unknown_pipe; /* 34 */ + unsigned char write_file_pipe; /* 35 */ + unsigned char write_input_pipe; /* 36 */ +} __attribute__ ((packed)) am_recovery_device; + +/* A CoreFoundation object of class AMRestoreModeDevice. */ +typedef struct am_restore_device { + unsigned char unknown[32]; + int port; +} __attribute__ ((packed)) am_restore_device; + +/* The type of the device notification callback function. */ +typedef void(*am_device_notification_callback)(struct + am_device_notification_callback_info *, void* arg); + +/* The type of the _AMDDeviceAttached function. + * TODO: change to correct type. */ +typedef void *amd_device_attached_callback; + + +typedef struct am_device { + unsigned char unknown0[16]; /* 0 - zero */ + unsigned int device_id; /* 16 */ + unsigned int product_id; /* 20 - set to AMD_IPHONE_PRODUCT_ID */ + char *serial; /* 24 - set to AMD_IPHONE_SERIAL */ + unsigned int unknown1; /* 28 */ + unsigned char unknown2[4]; /* 32 */ + unsigned int lockdown_conn; /* 36 */ + unsigned char unknown3[8]; /* 40 */ +} __attribute__ ((packed)) am_device; + +typedef struct am_device_notification { + unsigned int unknown0; /* 0 */ + unsigned int unknown1; /* 4 */ + unsigned int unknown2; /* 8 */ + am_device_notification_callback callback; /* 12 */ + unsigned int unknown3; /* 16 */ +} __attribute__ ((packed)) am_device_notification; + +typedef struct afc_connection { + unsigned int handle; /* 0 */ + unsigned int unknown0; /* 4 */ + unsigned char unknown1; /* 8 */ + unsigned char padding[3]; /* 9 */ + unsigned int unknown2; /* 12 */ + unsigned int unknown3; /* 16 */ + unsigned int unknown4; /* 20 */ + unsigned int fs_block_size; /* 24 */ + unsigned int sock_block_size; /* 28: always 0x3c */ + unsigned int io_timeout; /* 32: from AFCConnectionOpen, usu. 0 */ + void *afc_lock; /* 36 */ + unsigned int context; /* 40 */ +} __attribute__ ((packed)) afc_connection; + +typedef struct afc_directory { + unsigned char unknown[0]; /* size unknown */ +} __attribute__ ((packed)) afc_directory; + +typedef struct afc_dictionary { + unsigned char unknown[0]; /* size unknown */ +} __attribute__ ((packed)) afc_dictionary; + +typedef unsigned long long afc_file_ref; + +typedef struct usbmux_listener_1 { /* offset value in iTunes */ + unsigned int unknown0; /* 0 1 */ + unsigned char *unknown1; /* 4 ptr, maybe device? */ + amd_device_attached_callback callback; /* 8 _AMDDeviceAttached */ + unsigned int unknown3; /* 12 */ + unsigned int unknown4; /* 16 */ + unsigned int unknown5; /* 20 */ +} __attribute__ ((packed)) usbmux_listener_1; + +typedef struct usbmux_listener_2 { + unsigned char unknown0[4144]; +} __attribute__ ((packed)) usbmux_listener_2; + +typedef struct am_bootloader_control_packet { + unsigned char opcode; /* 0 */ + unsigned char length; /* 1 */ + unsigned char magic[2]; /* 2: 0x34, 0x12 */ + unsigned char payload[0]; /* 4 */ +} __attribute__ ((packed)) am_bootloader_control_packet; + +/* ---------------------------------------------------------------------------- + * Public routines + * ------------------------------------------------------------------------- */ + +void AMDSetLogLevel(int level); + +/* Registers a notification with the current run loop. The callback gets + * copied into the notification struct, as well as being registered with the + * current run loop. dn_unknown3 gets copied into unknown3 in the same. + * (Maybe dn_unknown3 is a user info parameter that gets passed as an arg to + * the callback?) unused0 and unused1 are both 0 when iTunes calls this. + * In iTunes the callback is located from $3db78e-$3dbbaf. + * + * Returns: + * MDERR_OK if successful + * MDERR_SYSCALL if CFRunLoopAddSource() failed + * MDERR_OUT_OF_MEMORY if we ran out of memory + */ + +mach_error_t AMDeviceNotificationSubscribe(am_device_notification_callback + callback, unsigned int unused0, unsigned int unused1, void* //unsigned int + dn_unknown3, struct am_device_notification **notification); + + +/* Connects to the iPhone. Pass in the am_device structure that the + * notification callback will give to you. + * + * Returns: + * MDERR_OK if successfully connected + * MDERR_SYSCALL if setsockopt() failed + * MDERR_QUERY_FAILED if the daemon query failed + * MDERR_INVALID_ARGUMENT if USBMuxConnectByPort returned 0xffffffff + */ + +mach_error_t AMDeviceConnect(struct am_device *device); + +/* Calls PairingRecordPath() on the given device, than tests whether the path + * which that function returns exists. During the initial connect, the path + * returned by that function is '/', and so this returns 1. + * + * Returns: + * 0 if the path did not exist + * 1 if it did + */ + +int AMDeviceIsPaired(struct am_device *device); + +/* iTunes calls this function immediately after testing whether the device is + * paired. It creates a pairing file and establishes a Lockdown connection. + * + * Returns: + * MDERR_OK if successful + * MDERR_INVALID_ARGUMENT if the supplied device is null + * MDERR_DICT_NOT_LOADED if the load_dict() call failed + */ + +mach_error_t AMDeviceValidatePairing(struct am_device *device); + +/* Creates a Lockdown session and adjusts the device structure appropriately + * to indicate that the session has been started. iTunes calls this function + * after validating pairing. + * + * Returns: + * MDERR_OK if successful + * MDERR_INVALID_ARGUMENT if the Lockdown conn has not been established + * MDERR_DICT_NOT_LOADED if the load_dict() call failed + */ + +mach_error_t AMDeviceStartSession(struct am_device *device); + +/* Starts a service and returns a handle that can be used in order to further + * access the service. You should stop the session and disconnect before using + * the service. iTunes calls this function after starting a session. It starts + * the service and the SSL connection. unknown may safely be + * NULL (it is when iTunes calls this), but if it is not, then it will be + * filled upon function exit. service_name should be one of the AMSVC_* + * constants. If the service is AFC (AMSVC_AFC), then the handle is the handle + * that will be used for further AFC* calls. + * + * Returns: + * MDERR_OK if successful + * MDERR_SYSCALL if the setsockopt() call failed + * MDERR_INVALID_ARGUMENT if the Lockdown conn has not been established + */ + +mach_error_t AMDeviceStartService(struct am_device *device, CFStringRef + service_name, service_conn_t *handle, unsigned int * + unknown); + +mach_error_t AMDeviceStartHouseArrestService(struct am_device *device, CFStringRef identifier, void *unknown, service_conn_t *handle, unsigned int *what); + +/* Stops a session. You should do this before accessing services. + * + * Returns: + * MDERR_OK if successful + * MDERR_INVALID_ARGUMENT if the Lockdown conn has not been established + */ + +mach_error_t AMDeviceStopSession(struct am_device *device); + +/* Opens an Apple File Connection. You must start the appropriate service + * first with AMDeviceStartService(). In iTunes, io_timeout is 0. + * + * Returns: + * MDERR_OK if successful + * MDERR_AFC_OUT_OF_MEMORY if malloc() failed + */ + +afc_error_t AFCConnectionOpen(service_conn_t handle, unsigned int io_timeout, + struct afc_connection **conn); + +/* Pass in a pointer to an afc_device_info structure. It will be filled. */ +afc_error_t AFCDeviceInfoOpen(afc_connection *conn, struct + afc_dictionary **info); + +/* Turns debug mode on if the environment variable AFCDEBUG is set to a numeric + * value, or if the file '/AFCDEBUG' is present and contains a value. */ + void AFCPlatformInit(void); + +/* Opens a directory on the iPhone. Pass in a pointer in dir to be filled in. + * Note that this normally only accesses the iTunes sandbox/partition as the + * root, which is /var/root/Media. Pathnames are specified with '/' delimiters + * as in Unix style. + * + * Returns: + * MDERR_OK if successful + */ + +afc_error_t AFCDirectoryOpen(afc_connection *conn, const char *path, + struct afc_directory **dir); + +/* Acquires the next entry in a directory previously opened with + * AFCDirectoryOpen(). When dirent is filled with a NULL value, then the end + * of the directory has been reached. '.' and '..' will be returned as the + * first two entries in each directory except the root; you may want to skip + * over them. + * + * Returns: + * MDERR_OK if successful, even if no entries remain + */ + +afc_error_t AFCDirectoryRead(afc_connection *conn/*unsigned int unused*/, struct afc_directory *dir, + char **dirent); + +afc_error_t AFCDirectoryClose(afc_connection *conn, struct afc_directory *dir); +afc_error_t AFCDirectoryCreate(afc_connection *conn, const char *dirname); +afc_error_t AFCRemovePath(afc_connection *conn, const char *dirname); +afc_error_t AFCRenamePath(afc_connection *conn, const char *from, const char *to); +afc_error_t AFCLinkPath(afc_connection *conn, long long int linktype, const char *target, const char *linkname); + +/* Returns the context field of the given AFC connection. */ +unsigned int AFCConnectionGetContext(afc_connection *conn); + +/* Returns the fs_block_size field of the given AFC connection. */ +unsigned int AFCConnectionGetFSBlockSize(afc_connection *conn); + +/* Returns the io_timeout field of the given AFC connection. In iTunes this is + * 0. */ +unsigned int AFCConnectionGetIOTimeout(afc_connection *conn); + +/* Returns the sock_block_size field of the given AFC connection. */ +unsigned int AFCConnectionGetSocketBlockSize(afc_connection *conn); + +/* Closes the given AFC connection. */ +afc_error_t AFCConnectionClose(afc_connection *conn); + +/* Registers for device notifications related to the restore process. unknown0 + * is zero when iTunes calls this. In iTunes, + * the callbacks are located at: + * 1: $3ac68e-$3ac6b1, calls $3ac542(unknown1, arg, 0) + * 2: $3ac66a-$3ac68d, calls $3ac542(unknown1, 0, arg) + * 3: $3ac762-$3ac785, calls $3ac6b2(unknown1, arg, 0) + * 4: $3ac73e-$3ac761, calls $3ac6b2(unknown1, 0, arg) + */ + +unsigned int AMRestoreRegisterForDeviceNotifications( + am_restore_device_notification_callback dfu_connect_callback, + am_restore_device_notification_callback recovery_connect_callback, + am_restore_device_notification_callback dfu_disconnect_callback, + am_restore_device_notification_callback recovery_disconnect_callback, + unsigned int unknown0, + void *user_info); + +/* Causes the restore functions to spit out (unhelpful) progress messages to + * the file specified by the given path. iTunes always calls this right before + * restoring with a path of + * "$HOME/Library/Logs/iPhone Updater Logs/iPhoneUpdater X.log", where X is an + * unused number. + */ + +unsigned int AMRestoreEnableFileLogging(char *path); + +/* Initializes a new option dictionary to default values. Pass the constant + * kCFAllocatorDefault as the allocator. The option dictionary looks as + * follows: + * { + * NORImageType => 'production', + * AutoBootDelay => 0, + * KernelCacheType => 'Release', + * UpdateBaseband => true, + * DFUFileType => 'RELEASE', + * SystemImageType => 'User', + * CreateFilesystemPartitions => true, + * FlashNOR => true, + * RestoreBootArgs => 'rd=md0 nand-enable-reformat=1 -progress' + * BootImageType => 'User' + * } + * + * Returns: + * the option dictionary if successful + * NULL if out of memory + */ + +CFMutableDictionaryRef AMRestoreCreateDefaultOptions(CFAllocatorRef allocator); + +/* ---------------------------------------------------------------------------- + * Less-documented public routines + * ------------------------------------------------------------------------- */ + +/* mode 2 = read, mode 3 = write */ +afc_error_t AFCFileRefOpen(afc_connection *conn, const char *path, + unsigned long long mode, afc_file_ref *ref); +afc_error_t AFCFileRefSeek(afc_connection *conn, afc_file_ref ref, + unsigned long long offset1, unsigned long long offset2); +afc_error_t AFCFileRefRead(afc_connection *conn, afc_file_ref ref, + void *buf, size_t *len); +afc_error_t AFCFileRefSetFileSize(afc_connection *conn, afc_file_ref ref, + unsigned long long offset); +afc_error_t AFCFileRefWrite(afc_connection *conn, afc_file_ref ref, + const void *buf, size_t len); +afc_error_t AFCFileRefClose(afc_connection *conn, afc_file_ref ref); + +afc_error_t AFCFileInfoOpen(afc_connection *conn, const char *path, struct + afc_dictionary **info); +afc_error_t AFCKeyValueRead(struct afc_dictionary *dict, char **key, char ** + val); +afc_error_t AFCKeyValueClose(struct afc_dictionary *dict); + +unsigned int AMRestorePerformRecoveryModeRestore(struct am_recovery_device * + rdev, CFDictionaryRef opts, void *callback, void *user_info); +unsigned int AMRestorePerformRestoreModeRestore(struct am_restore_device * + rdev, CFDictionaryRef opts, void *callback, void *user_info); + +struct am_restore_device *AMRestoreModeDeviceCreate(unsigned int unknown0, + unsigned int connection_id, unsigned int unknown1); + +unsigned int AMRestoreCreatePathsForBundle(CFStringRef restore_bundle_path, + CFStringRef kernel_cache_type, CFStringRef boot_image_type, unsigned int + unknown0, CFStringRef *firmware_dir_path, CFStringRef * + kernelcache_restore_path, unsigned int unknown1, CFStringRef * + ramdisk_path); + +unsigned int AMDeviceGetConnectionID(struct am_device *device); +mach_error_t AMDeviceEnterRecovery(struct am_device *device); +mach_error_t AMDeviceDisconnect(struct am_device *device); +mach_error_t AMDeviceRetain(struct am_device *device); +mach_error_t AMDeviceRelease(struct am_device *device); +CFStringRef AMDeviceCopyValue(struct am_device *device, unsigned int, CFStringRef cfstring); +CFStringRef AMDeviceCopyDeviceIdentifier(struct am_device *device); + +typedef void (*notify_callback)(CFStringRef notification, void *data); + +mach_error_t AMDPostNotification(service_conn_t socket, CFStringRef notification, CFStringRef userinfo); +mach_error_t AMDObserveNotification(void *socket, CFStringRef notification); +mach_error_t AMDListenForNotifications(void *socket, notify_callback cb, void *data); +mach_error_t AMDShutdownNotificationProxy(void *socket); + +/*edits by geohot*/ +mach_error_t AMDeviceDeactivate(struct am_device *device); +mach_error_t AMDeviceActivate(struct am_device *device, CFMutableDictionaryRef); +/*end*/ + +void *AMDeviceSerialize(struct am_device *device); +void AMDAddLogFileDescriptor(int fd); +//kern_return_t AMDeviceSendMessage(service_conn_t socket, void *unused, CFPropertyListRef plist); +//kern_return_t AMDeviceReceiveMessage(service_conn_t socket, CFDictionaryRef options, CFPropertyListRef * result); + +typedef int (*am_device_install_application_callback)(CFDictionaryRef, int); + +mach_error_t AMDeviceInstallApplication(service_conn_t socket, CFStringRef path, CFDictionaryRef options, am_device_install_application_callback callback, void *user); +mach_error_t AMDeviceTransferApplication(service_conn_t socket, CFStringRef path, CFDictionaryRef options, am_device_install_application_callback callbackj, void *user); + +int AMDeviceSecureUninstallApplication(int unknown0, struct am_device *device, CFStringRef bundle_id, int unknown1, void *callback, int callback_arg); + +/* ---------------------------------------------------------------------------- + * Semi-private routines + * ------------------------------------------------------------------------- */ + +/* Pass in a usbmux_listener_1 structure and a usbmux_listener_2 structure + * pointer, which will be filled with the resulting usbmux_listener_2. + * + * Returns: + * MDERR_OK if completed successfully + * MDERR_USBMUX_ARG_NULL if one of the arguments was NULL + * MDERR_USBMUX_FAILED if the listener was not created successfully + */ + +usbmux_error_t USBMuxListenerCreate(struct usbmux_listener_1 *esi_fp8, struct + usbmux_listener_2 **eax_fp12); + +/* ---------------------------------------------------------------------------- + * Less-documented semi-private routines + * ------------------------------------------------------------------------- */ + +usbmux_error_t USBMuxListenerHandleData(void *); + +/* ---------------------------------------------------------------------------- + * Private routines - here be dragons + * ------------------------------------------------------------------------- */ + +/* AMRestorePerformRestoreModeRestore() calls this function with a dictionary + * in order to perform certain special restore operations + * (RESTORED_OPERATION_*). It is thought that this function might enable + * significant access to the phone. */ + +typedef unsigned int (*t_performOperation)(struct am_restore_device *rdev, + CFDictionaryRef op); // __attribute__ ((regparm(2))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/device_db.h b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/device_db.h new file mode 100644 index 000000000000..489cf36cdfc8 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/device_db.h @@ -0,0 +1,118 @@ +// +// devices.h +// ios-deploy +// +// Created by Gusts Kaksis on 26/10/2016. +// Copyright © 2016 PhoneGap. All rights reserved. +// + +#import + +#define ADD_DEVICE(model, name, sdk, arch) {CFSTR(model), CFSTR(name), CFSTR(sdk), CFSTR(arch)} + +typedef struct { + CFStringRef model; + CFStringRef name; + CFStringRef sdk; + CFStringRef arch; +} device_desc; + +#define UNKNOWN_DEVICE_IDX 0 + +device_desc device_db[] = { + ADD_DEVICE("UNKN", "Unknown Device", "uknownos", "unkarch"), + + // iPod Touch + + ADD_DEVICE("N45AP", "iPod Touch", "iphoneos", "armv7"), + ADD_DEVICE("N72AP", "iPod Touch 2G", "iphoneos", "armv7"), + ADD_DEVICE("N18AP", "iPod Touch 3G", "iphoneos", "armv7"), + ADD_DEVICE("N81AP", "iPod Touch 4G", "iphoneos", "armv7"), + ADD_DEVICE("N78AP", "iPod Touch 5G", "iphoneos", "armv7"), + ADD_DEVICE("N78AAP", "iPod Touch 5G", "iphoneos", "armv7"), + ADD_DEVICE("N102AP", "iPod Touch 6G", "iphoneos", "arm64"), + + // iPad + + ADD_DEVICE("K48AP", "iPad", "iphoneos", "armv7"), + ADD_DEVICE("K93AP", "iPad 2", "iphoneos", "armv7"), + ADD_DEVICE("K94AP", "iPad 2 (GSM)", "iphoneos", "armv7"), + ADD_DEVICE("K95AP", "iPad 2 (CDMA)", "iphoneos", "armv7"), + ADD_DEVICE("K93AAP", "iPad 2 (Wi-Fi, revision A)", "iphoneos", "armv7"), + ADD_DEVICE("J1AP", "iPad 3", "iphoneos", "armv7"), + ADD_DEVICE("J2AP", "iPad 3 (GSM)", "iphoneos", "armv7"), + ADD_DEVICE("J2AAP", "iPad 3 (CDMA)", "iphoneos", "armv7"), + ADD_DEVICE("P101AP", "iPad 4", "iphoneos", "armv7s"), + ADD_DEVICE("P102AP", "iPad 4 (GSM)", "iphoneos", "armv7s"), + ADD_DEVICE("P103AP", "iPad 4 (CDMA)", "iphoneos", "armv7s"), + ADD_DEVICE("J71AP", "iPad Air", "iphoneos", "arm64"), + ADD_DEVICE("J72AP", "iPad Air (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("J73AP", "iPad Air (CDMA)", "iphoneos", "arm64"), + ADD_DEVICE("J81AP", "iPad Air 2", "iphoneos", "arm64"), + ADD_DEVICE("J82AP", "iPad Air 2 (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("J83AP", "iPad Air 2 (CDMA)", "iphoneos", "arm64"), + ADD_DEVICE("J71sAP", "iPad (2017)", "iphoneos", "arm64"), + ADD_DEVICE("J71tAP", "iPad (2017)", "iphoneos", "arm64"), + ADD_DEVICE("J72sAP", "iPad (2017)", "iphoneos", "arm64"), + ADD_DEVICE("J72tAP", "iPad (2017)", "iphoneos", "arm64"), + + // iPad Pro + + ADD_DEVICE("J98aAP", "iPad Pro (12.9\")", "iphoneos", "arm64"), + ADD_DEVICE("J99aAP", "iPad Pro (12.9\")", "iphoneos", "arm64"), + ADD_DEVICE("J120AP", "iPad Pro 2G (12.9\")", "iphoneos", "arm64"), + ADD_DEVICE("J121AP", "iPad Pro 2G (12.9\")", "iphoneos", "arm64"), + ADD_DEVICE("J127AP", "iPad Pro (9.7\")", "iphoneos", "arm64"), + ADD_DEVICE("J128AP", "iPad Pro (9.7\")", "iphoneos", "arm64"), + ADD_DEVICE("J207AP", "iPad Pro (10.5\")", "iphoneos", "arm64"), + ADD_DEVICE("J208AP", "iPad Pro (10.5\")", "iphoneos", "arm64"), + + // iPad Mini + + ADD_DEVICE("P105AP", "iPad mini", "iphoneos", "armv7"), + ADD_DEVICE("P106AP", "iPad mini (GSM)", "iphoneos", "armv7"), + ADD_DEVICE("P107AP", "iPad mini (CDMA)", "iphoneos", "armv7"), + ADD_DEVICE("J85AP", "iPad mini 2", "iphoneos", "arm64"), + ADD_DEVICE("J86AP", "iPad mini 2 (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("J87AP", "iPad mini 2 (CDMA)", "iphoneos", "arm64"), + ADD_DEVICE("J85MAP", "iPad mini 3", "iphoneos", "arm64"), + ADD_DEVICE("J86MAP", "iPad mini 3 (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("J87MAP", "iPad mini 3 (CDMA)", "iphoneos", "arm64"), + ADD_DEVICE("J96AP", "iPad mini 4", "iphoneos", "arm64"), + ADD_DEVICE("J97AP", "iPad mini 4 (GSM)", "iphoneos", "arm64"), + + // iPhone + + ADD_DEVICE("M68AP", "iPhone", "iphoneos", "armv7"), + ADD_DEVICE("N82AP", "iPhone 3G", "iphoneos", "armv7"), + ADD_DEVICE("N88AP", "iPhone 3GS", "iphoneos", "armv7"), + ADD_DEVICE("N90AP", "iPhone 4 (GSM)", "iphoneos", "armv7"), + ADD_DEVICE("N92AP", "iPhone 4 (CDMA)", "iphoneos", "armv7"), + ADD_DEVICE("N90BAP", "iPhone 4 (GSM, revision A)", "iphoneos", "armv7"), + ADD_DEVICE("N94AP", "iPhone 4S", "iphoneos", "armv7"), + ADD_DEVICE("N41AP", "iPhone 5 (GSM)", "iphoneos", "armv7s"), + ADD_DEVICE("N42AP", "iPhone 5 (Global/CDMA)", "iphoneos", "armv7s"), + ADD_DEVICE("N48AP", "iPhone 5c (GSM)", "iphoneos", "armv7s"), + ADD_DEVICE("N49AP", "iPhone 5c (Global/CDMA)", "iphoneos", "armv7s"), + ADD_DEVICE("N51AP", "iPhone 5s (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("N53AP", "iPhone 5s (Global/CDMA)", "iphoneos", "arm64"), + ADD_DEVICE("N61AP", "iPhone 6 (GSM)", "iphoneos", "arm64"), + ADD_DEVICE("N56AP", "iPhone 6 Plus", "iphoneos", "arm64"), + ADD_DEVICE("N71mAP", "iPhone 6s", "iphoneos", "arm64"), + ADD_DEVICE("N71AP", "iPhone 6s", "iphoneos", "arm64"), + ADD_DEVICE("N66AP", "iPhone 6s Plus", "iphoneos", "arm64"), + ADD_DEVICE("N66mAP", "iPhone 6s Plus", "iphoneos", "arm64"), + ADD_DEVICE("N69AP", "iPhone SE", "iphoneos", "arm64"), + ADD_DEVICE("N69uAP", "iPhone SE", "iphoneos", "arm64"), + ADD_DEVICE("D10AP", "iPhone 7", "iphoneos", "arm64"), + ADD_DEVICE("D101AP", "iPhone 7", "iphoneos", "arm64"), + ADD_DEVICE("D11AP", "iPhone 7 Plus", "iphoneos", "arm64"), + ADD_DEVICE("D111AP", "iPhone 7 Plus", "iphoneos", "arm64"), + + // Apple TV + + ADD_DEVICE("K66AP", "Apple TV 2G", "appletvos", "armv7"), + ADD_DEVICE("J33AP", "Apple TV 3G", "appletvos", "armv7"), + ADD_DEVICE("J33IAP", "Apple TV 3.1G", "appletvos", "armv7"), + ADD_DEVICE("J42dAP", "Apple TV 4G", "appletvos", "arm64"), + }; diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/errors.h b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/errors.h new file mode 100644 index 000000000000..717f81df8a65 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/errors.h @@ -0,0 +1,531 @@ + +typedef struct errorcode_to_id { + unsigned int error; + const char* id; +} errorcode_to_id_t; + +typedef struct error_id_to_message { + const char* id; + const char* message; +} error_id_to_message_t; + +// Parts of error code to localization id map is taken from SDMMobileDevice framework. Associated license is bellow. +// https://github.com/samdmarshall/SDMMobileDevice/blob/master/Framework/MobileDevice/Error/SDMMD_Error.h +// +// Copyright (c) 2014, Sam Marshall +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of Sam Marshall nor the names of its contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +static errorcode_to_id_t errorcode_to_id[] = { + { 0x00000000, "kAMDSuccess" }, + { 0xe8000001, "kAMDUndefinedError" }, + { 0xe8000002, "kAMDBadHeaderError" }, + { 0xe8000003, "kAMDNoResourcesError" }, + { 0xe8000004, "kAMDReadError" }, + { 0xe8000005, "kAMDWriteError" }, + { 0xe8000006, "kAMDUnknownPacketError" }, + { 0xe8000007, "kAMDInvalidArgumentError" }, + { 0xe8000008, "kAMDNotFoundError" }, + { 0xe8000009, "kAMDIsDirectoryError" }, + { 0xe800000a, "kAMDPermissionError" }, + { 0xe800000b, "kAMDNotConnectedError" }, + { 0xe800000c, "kAMDTimeOutError" }, + { 0xe800000d, "kAMDOverrunError" }, + { 0xe800000e, "kAMDEOFError" }, + { 0xe800000f, "kAMDUnsupportedError" }, + { 0xe8000010, "kAMDFileExistsError" }, + { 0xe8000011, "kAMDBusyError" }, + { 0xe8000012, "kAMDCryptoError" }, + { 0xe8000013, "kAMDInvalidResponseError" }, + { 0xe8000014, "kAMDMissingKeyError" }, + { 0xe8000015, "kAMDMissingValueError" }, + { 0xe8000016, "kAMDGetProhibitedError" }, + { 0xe8000017, "kAMDSetProhibitedError" }, + { 0xe8000018, "kAMDRemoveProhibitedError" }, + { 0xe8000019, "kAMDImmutableValueError" }, + { 0xe800001a, "kAMDPasswordProtectedError" }, + { 0xe800001b, "kAMDMissingHostIDError" }, + { 0xe800001c, "kAMDInvalidHostIDError" }, + { 0xe800001d, "kAMDSessionActiveError" }, + { 0xe800001e, "kAMDSessionInactiveError" }, + { 0xe800001f, "kAMDMissingSessionIDError" }, + { 0xe8000020, "kAMDInvalidSessionIDError" }, + { 0xe8000021, "kAMDMissingServiceError" }, + { 0xe8000022, "kAMDInvalidServiceError" }, + { 0xe8000023, "kAMDInvalidCheckinError" }, + { 0xe8000024, "kAMDCheckinTimeoutError" }, + { 0xe8000025, "kAMDMissingPairRecordError" }, + { 0xe8000026, "kAMDInvalidActivationRecordError" }, + { 0xe8000027, "kAMDMissingActivationRecordError" }, + { 0xe8000028, "kAMDWrongDroidError" }, + { 0xe8000029, "kAMDSUVerificationError" }, + { 0xe800002a, "kAMDSUPatchError" }, + { 0xe800002b, "kAMDSUFirmwareError" }, + { 0xe800002c, "kAMDProvisioningProfileNotValid" }, + { 0xe800002d, "kAMDSendMessageError" }, + { 0xe800002e, "kAMDReceiveMessageError" }, + { 0xe800002f, "kAMDMissingOptionsError" }, + { 0xe8000030, "kAMDMissingImageTypeError" }, + { 0xe8000031, "kAMDDigestFailedError" }, + { 0xe8000032, "kAMDStartServiceError" }, + { 0xe8000033, "kAMDInvalidDiskImageError" }, + { 0xe8000034, "kAMDMissingDigestError" }, + { 0xe8000035, "kAMDMuxError" }, + { 0xe8000036, "kAMDApplicationAlreadyInstalledError" }, + { 0xe8000037, "kAMDApplicationMoveFailedError" }, + { 0xe8000038, "kAMDApplicationSINFCaptureFailedError" }, + { 0xe8000039, "kAMDApplicationSandboxFailedError" }, + { 0xe800003a, "kAMDApplicationVerificationFailedError" }, + { 0xe800003b, "kAMDArchiveDestructionFailedError" }, + { 0xe800003c, "kAMDBundleVerificationFailedError" }, + { 0xe800003d, "kAMDCarrierBundleCopyFailedError" }, + { 0xe800003e, "kAMDCarrierBundleDirectoryCreationFailedError" }, + { 0xe800003f, "kAMDCarrierBundleMissingSupportedSIMsError" }, + { 0xe8000040, "kAMDCommCenterNotificationFailedError" }, + { 0xe8000041, "kAMDContainerCreationFailedError" }, + { 0xe8000042, "kAMDContainerP0wnFailedError" }, + { 0xe8000043, "kAMDContainerRemovalFailedError" }, + { 0xe8000044, "kAMDEmbeddedProfileInstallFailedError" }, + { 0xe8000045, "kAMDErrorError" }, + { 0xe8000046, "kAMDExecutableTwiddleFailedError" }, + { 0xe8000047, "kAMDExistenceCheckFailedError" }, + { 0xe8000048, "kAMDInstallMapUpdateFailedError" }, + { 0xe8000049, "kAMDManifestCaptureFailedError" }, + { 0xe800004a, "kAMDMapGenerationFailedError" }, + { 0xe800004b, "kAMDMissingBundleExecutableError" }, + { 0xe800004c, "kAMDMissingBundleIdentifierError" }, + { 0xe800004d, "kAMDMissingBundlePathError" }, + { 0xe800004e, "kAMDMissingContainerError" }, + { 0xe800004f, "kAMDNotificationFailedError" }, + { 0xe8000050, "kAMDPackageExtractionFailedError" }, + { 0xe8000051, "kAMDPackageInspectionFailedError" }, + { 0xe8000052, "kAMDPackageMoveFailedError" }, + { 0xe8000053, "kAMDPathConversionFailedError" }, + { 0xe8000054, "kAMDRestoreContainerFailedError" }, + { 0xe8000055, "kAMDSeatbeltProfileRemovalFailedError" }, + { 0xe8000056, "kAMDStageCreationFailedError" }, + { 0xe8000057, "kAMDSymlinkFailedError" }, + { 0xe8000058, "kAMDiTunesArtworkCaptureFailedError" }, + { 0xe8000059, "kAMDiTunesMetadataCaptureFailedError" }, + { 0xe800005a, "kAMDAlreadyArchivedError" }, + { 0xe800005b, "kAMDServiceLimitError" }, + { 0xe800005c, "kAMDInvalidPairRecordError" }, + { 0xe800005d, "kAMDServiceProhibitedError" }, + { 0xe800005e, "kAMDCheckinSetupFailedError" }, + { 0xe800005f, "kAMDCheckinConnectionFailedError" }, + { 0xe8000060, "kAMDCheckinReceiveFailedError" }, + { 0xe8000061, "kAMDCheckinResponseFailedError" }, + { 0xe8000062, "kAMDCheckinSendFailedError" }, + { 0xe8000063, "kAMDMuxCreateListenerError" }, + { 0xe8000064, "kAMDMuxGetListenerError" }, + { 0xe8000065, "kAMDMuxConnectError" }, + { 0xe8000066, "kAMDUnknownCommandError" }, + { 0xe8000067, "kAMDAPIInternalError" }, + { 0xe8000068, "kAMDSavePairRecordFailedError" }, + { 0xe8000069, "kAMDCheckinOutOfMemoryError" }, + { 0xe800006a, "kAMDDeviceTooNewError" }, + { 0xe800006b, "kAMDDeviceRefNoGood" }, + { 0xe800006c, "kAMDCannotTranslateError" }, + { 0xe800006d, "kAMDMobileImageMounterMissingImageSignature" }, + { 0xe800006e, "kAMDMobileImageMounterResponseCreationFailed" }, + { 0xe800006f, "kAMDMobileImageMounterMissingImageType" }, + { 0xe8000070, "kAMDMobileImageMounterMissingImagePath" }, + { 0xe8000071, "kAMDMobileImageMounterImageMapLoadFailed" }, + { 0xe8000072, "kAMDMobileImageMounterAlreadyMounted" }, + { 0xe8000073, "kAMDMobileImageMounterImageMoveFailed" }, + { 0xe8000074, "kAMDMobileImageMounterMountPathMissing" }, + { 0xe8000075, "kAMDMobileImageMounterMountPathNotEmpty" }, + { 0xe8000076, "kAMDMobileImageMounterImageMountFailed" }, + { 0xe8000077, "kAMDMobileImageMounterTrustCacheLoadFailed" }, + { 0xe8000078, "kAMDMobileImageMounterDigestFailed" }, + { 0xe8000079, "kAMDMobileImageMounterDigestCreationFailed" }, + { 0xe800007a, "kAMDMobileImageMounterImageVerificationFailed" }, + { 0xe800007b, "kAMDMobileImageMounterImageInfoCreationFailed" }, + { 0xe800007c, "kAMDMobileImageMounterImageMapStoreFailed" }, + { 0xe800007d, "kAMDBonjourSetupError" }, + { 0xe800007e, "kAMDDeviceOSVersionTooLow" }, + { 0xe800007f, "kAMDNoWifiSyncSupportError" }, + { 0xe8000080, "kAMDDeviceFamilyNotSupported" }, + { 0xe8000081, "kAMDEscrowLockedError" }, + { 0xe8000082, "kAMDPairingProhibitedError" }, + { 0xe8000083, "kAMDProhibitedBySupervision" }, + { 0xe8000084, "kAMDDeviceDisconnectedError" }, + { 0xe8000085, "kAMDTooBigError" }, + { 0xe8000086, "kAMDPackagePatchFailedError" }, + { 0xe8000087, "kAMDIncorrectArchitectureError" }, + { 0xe8000088, "kAMDPluginCopyFailedError" }, + { 0xe8000089, "kAMDBreadcrumbFailedError" }, + { 0xe800008a, "kAMDBreadcrumbUnlockError" }, + { 0xe800008b, "kAMDGeoJSONCaptureFailedError" }, + { 0xe800008c, "kAMDNewsstandArtworkCaptureFailedError" }, + { 0xe800008d, "kAMDMissingCommandError" }, + { 0xe800008e, "kAMDNotEntitledError" }, + { 0xe800008f, "kAMDMissingPackagePathError" }, + { 0xe8000090, "kAMDMissingContainerPathError" }, + { 0xe8000091, "kAMDMissingApplicationIdentifierError" }, + { 0xe8000092, "kAMDMissingAttributeValueError" }, + { 0xe8000093, "kAMDLookupFailedError" }, + { 0xe8000094, "kAMDDictCreationFailedError" }, + { 0xe8000095, "kAMDUserDeniedPairingError" }, + { 0xe8000096, "kAMDPairingDialogResponsePendingError" }, + { 0xe8000097, "kAMDInstallProhibitedError" }, + { 0xe8000098, "kAMDUninstallProhibitedError" }, + { 0xe8000099, "kAMDFMiPProtectedError" }, + { 0xe800009a, "kAMDMCProtected" }, + { 0xe800009b, "kAMDMCChallengeRequired" }, + { 0xe800009c, "kAMDMissingBundleVersionError" }, + { 0xe800009d, "kAMDAppBlacklistedError" }, + { 0xe800009e, "This app contains an app extension with an illegal bundle identifier. App extension bundle identifiers must have a prefix consisting of their containing application's bundle identifier followed by a '.'." }, + { 0xe800009f, "If an app extension defines the XPCService key in its Info.plist, it must have a dictionary value." }, + { 0xe80000a0, "App extensions must define the NSExtension key with a dictionary value in their Info.plist." }, + { 0xe80000a1, "If an app extension defines the CFBundlePackageType key in its Info.plist, it must have the value \"XPC!\"." }, + { 0xe80000a2, "App extensions must define either NSExtensionMainStoryboard or NSExtensionPrincipalClass keys in the NSExtension dictionary in their Info.plist." }, + { 0xe80000a3, "If an app extension defines the NSExtensionContextClass key in the NSExtension dictionary in its Info.plist, it must have a string value containing one or more characters." }, + { 0xe80000a4, "If an app extension defines the NSExtensionContextHostClass key in the NSExtension dictionary in its Info.plist, it must have a string value containing one or more characters." }, + { 0xe80000a5, "If an app extension defines the NSExtensionViewControllerHostClass key in the NSExtension dictionary in its Info.plist, it must have a string value containing one or more characters." }, + { 0xe80000a6, "This app contains an app extension that does not define the NSExtensionPointIdentifier key in its Info.plist. This key must have a reverse-DNS format string value." }, + { 0xe80000a7, "This app contains an app extension that does not define the NSExtensionPointIdentifier key in its Info.plist with a valid reverse-DNS format string value." }, + { 0xe80000a8, "If an app extension defines the NSExtensionAttributes key in the NSExtension dictionary in its Info.plist, it must have a dictionary value." }, + { 0xe80000a9, "If an app extension defines the NSExtensionPointName key in the NSExtensionAttributes dictionary in the NSExtension dictionary in its Info.plist, it must have a string value containing one or more characters." }, + { 0xe80000aa, "If an app extension defines the NSExtensionPointVersion key in the NSExtensionAttributes dictionary in the NSExtension dictionary in its Info.plist, it must have a string value containing one or more characters." }, + { 0xe80000ab, "This app or a bundle it contains does not define the CFBundleName key in its Info.plist with a string value containing one or more characters." }, + { 0xe80000ac, "This app or a bundle it contains does not define the CFBundleDisplayName key in its Info.plist with a string value containing one or more characters." }, + { 0xe80000ad, "This app or a bundle it contains defines the CFBundleShortVersionStringKey key in its Info.plist with a non-string value or a zero-length string value." }, + { 0xe80000ae, "This app or a bundle it contains defines the RunLoopType key in the XPCService dictionary in its Info.plist with a non-string value or a zero-length string value." }, + { 0xe80000af, "This app or a bundle it contains defines the ServiceType key in the XPCService dictionary in its Info.plist with a non-string value or a zero-length string value." }, + { 0xe80000b0, "This application or a bundle it contains has the same bundle identifier as this application or another bundle that it contains. Bundle identifiers must be unique." }, + { 0xe80000b1, "This app contains an app extension that specifies an extension point identifier that is not supported on this version of iOS for the value of the NSExtensionPointIdentifier key in its Info.plist." }, + { 0xe80000b2, "This app contains multiple app extensions that are file providers. Apps are only allowed to contain at most a single file provider app extension." }, + { 0xe80000b3, "kMobileHouseArrestMissingCommand" }, + { 0xe80000b4, "kMobileHouseArrestUnknownCommand" }, + { 0xe80000b5, "kMobileHouseArrestMissingIdentifier" }, + { 0xe80000b6, "kMobileHouseArrestDictionaryFailed" }, + { 0xe80000b7, "kMobileHouseArrestInstallationLookupFailed" }, + { 0xe80000b8, "kMobileHouseArrestApplicationLookupFailed" }, + { 0xe80000b9, "kMobileHouseArrestMissingContainer" }, + // 0xe80000ba does not exist + { 0xe80000bb, "kMobileHouseArrestPathConversionFailed" }, + { 0xe80000bc, "kMobileHouseArrestPathMissing" }, + { 0xe80000bd, "kMobileHouseArrestInvalidPath" }, + { 0xe80000be, "kAMDMismatchedApplicationIdentifierEntitlementError" }, + { 0xe80000bf, "kAMDInvalidSymlinkError" }, + { 0xe80000c0, "kAMDNoSpaceError" }, + { 0xe80000c1, "The WatchKit app extension must have, in its Info.plist's NSExtension dictionary's NSExtensionAttributes dictionary, the key WKAppBundleIdentifier with a value equal to the associated WatchKit app's bundle identifier." }, + { 0xe80000c2, "This app is not a valid AppleTV Stub App" }, + { 0xe80000c3, "kAMDBundleiTunesMetadataVersionMismatchError" }, + { 0xe80000c4, "kAMDInvalidiTunesMetadataPlistError" }, + { 0xe80000c5, "kAMDMismatchedBundleIDSigningIdentifierError" }, + { 0xe80000c6, "This app contains multiple WatchKit app extensions. Only a single WatchKit extension is allowed." }, + { 0xe80000c7, "A WatchKit app within this app is not a valid bundle." }, + { 0xe80000c8, "kAMDDeviceNotSupportedByThinningError" }, + { 0xe80000c9, "The UISupportedDevices key in this app's Info.plist does not specify a valid set of supported devices." }, + { 0xe80000ca, "This app contains an app extension with an illegal bundle identifier. App extension bundle identifiers must have a prefix consisting of their containing application's bundle identifier followed by a '.', with no further '.' characters after the prefix." }, + { 0xe80000cb, "kAMDAppexBundleIDConflictWithOtherIdentifierError" }, + { 0xe80000cc, "kAMDBundleIDConflictWithOtherIdentifierError" }, + { 0xe80000cd, "This app contains multiple WatchKit 1.0 apps. Only a single WatchKit 1.0 app is allowed." }, + { 0xe80000ce, "This app contains multiple WatchKit 2.0 apps. Only a single WatchKit 2.0 app is allowed." }, + { 0xe80000cf, "The WatchKit app has an invalid stub executable." }, + { 0xe80000d0, "The WatchKit app has multiple app extensions. Only a single WatchKit extension is allowed in a WatchKit app, and only if this is a WatchKit 2.0 app." }, + { 0xe80000d1, "The WatchKit 2.0 app contains non-WatchKit app extensions. Only WatchKit app extensions are allowed in WatchKit apps." }, + { 0xe80000d2, "The WatchKit app has one or more embedded frameworks. Frameworks are only allowed in WatchKit app extensions in WatchKit 2.0 apps." }, + { 0xe80000d3, "This app contains a WatchKit 1.0 app with app extensions. This is not allowed." }, + { 0xe80000d4, "This app contains a WatchKit 2.0 app without an app extension. WatchKit 2.0 apps must contain a WatchKit app extension." }, + { 0xe80000d5, "The WatchKit app's Info.plist must have a WKCompanionAppBundleIdentifier key set to the bundle identifier of the companion app." }, + { 0xe80000d6, "The WatchKit app's Info.plist contains a non-string key." }, + { 0xe80000d7, "The WatchKit app's Info.plist contains a key that is not in the whitelist of allowed keys for a WatchKit app." }, + { 0xe80000d8, "The WatchKit 1.0 and a WatchKit 2.0 apps within this app must have have the same bundle identifier." }, + { 0xe80000d9, "This app contains a WatchKit app with an invalid bundle identifier. The bundle identifier of a WatchKit app must have a prefix consisting of the companion app's bundle identifier, followed by a '.'." }, + { 0xe80000da, "This app contains a WatchKit app where the UIDeviceFamily key in its Info.plist does not specify the value 4 to indicate that it's compatible with the Apple Watch device type." }, + { 0xe80000db, "The device is out of storage for apps. Please remove some apps from the device and try again." }, + { 0xe80000dc, "This app or an app that it contains has a Siri Intents app extension that is missing the IntentsSupported array in the NSExtensionAttributes dictionary in the NSExtension dictionary in its Info.plist." }, + { 0xe80000dd, "This app or an app that it contains has a Siri Intents app extension that does not correctly define the IntentsRestrictedWhileLocked key in the NSExtensionAttributes dictionary in the NSExtension dictionary in its Info.plist. The key's value must be an array of strings." }, + { 0xe80000de, "This app or an app that it contains has a Siri Intents app extension that declares values in its IntentsRestrictedWhileLocked key's array value that are not in its IntentsSupported key's array value (in the NSExtensionAttributes dictionary in the NSExtension dictionary in its Info.plist)." }, + { 0xe80000df, "This app or an app that it contains declares multiple Siri Intents app extensions that declare one or more of the same values in the IntentsSupported array in the NSExtensionAttributes dictionary in the NSExtension dictionary in their Info.plist. IntentsSupported must be distinct among a given Siri Intents extension type within an app." }, + { 0xe80000e0, "The WatchKit 2.0 app, which expects to be compatible with watchOS versions earlier than 3.0, contains a non-WatchKit extension in a location that's not compatible with watchOS versions earlier than 3.0." }, + { 0xe80000e1, "The WatchKit 2.0 app, which expects to be compatible with watchOS versions earlier than 3.0, contains a framework in a location that's not compatible with watchOS versions earlier than 3.0." }, + { 0xe80000e2, "kAMDMobileImageMounterDeviceLocked" }, + { 0xe80000e3, "kAMDInvalidSINFError" }, + { 0xe80000e4, "Multiple iMessage app extensions were found in this app. Only one is allowed." }, + { 0xe80000e5, "This iMessage application is missing its required iMessage app extension." }, + { 0xe80000e6, "This iMessage application contains an app extension type other than an iMessage app extension. iMessage applications may only contain one iMessage app extension and may not contain other types of app extensions." }, + { 0xe80000e7, "This app contains a WatchKit app with one or more Siri Intents app extensions that declare IntentsSupported that are not declared in any of the companion app's Siri Intents app extensions. WatchKit Siri Intents extensions' IntentsSupported values must be a subset of the companion app's Siri Intents extensions' IntentsSupported values." }, + { 0xe80000e8, "kAMDRequireCUPairingCodeError" }, + { 0xe80000e9, "kAMDRequireCUPairingBackoffError" }, + { 0xe80000ea, "kAMDCUPairingError" }, + { 0xe80000eb, "kAMDCUPairingContinueError" }, + { 0xe80000ec, "kAMDCUPairingResetError" }, + { 0xe80000ed, "kAMDRequireCUPairingError" }, + { 0xe80000ee, "kAMDPasswordRequiredError" }, + + // Errors without id->string mapping. + { 0xe8008001, "An unknown error has occurred." }, + { 0xe8008002, "Attempted to modify an immutable provisioning profile." }, + { 0xe8008003, "This provisioning profile is malformed." }, + { 0xe8008004, "This provisioning profile does not have a valid signature (or it has a valid, but untrusted signature)." }, + { 0xe8008005, "This provisioning profile is malformed." }, + { 0xe8008006, "This provisioning profile is malformed." }, + { 0xe8008007, "This provisioning profile is malformed." }, + { 0xe8008008, "This provisioning profile is malformed." }, + { 0xe8008009, "The signature was not valid." }, + { 0xe800800a, "Unable to allocate memory." }, + { 0xe800800b, "A file operation failed." }, + { 0xe800800c, "There was an error communicating with your device." }, + { 0xe800800d, "There was an error communicating with your device." }, + { 0xe800800e, "This provisioning profile does not have a valid signature (or it has a valid, but untrusted signature)." }, + { 0xe800800f, "The application's signature is valid but it does not match the expected hash." }, + { 0xe8008010, "This provisioning profile is unsupported." }, + { 0xe8008011, "This provisioning profile has expired." }, + { 0xe8008012, "This provisioning profile cannot be installed on this device." }, + { 0xe8008013, "This provisioning profile does not have a valid signature (or it has a valid, but untrusted signature)." }, + { 0xe8008014, "The executable contains an invalid signature." }, + { 0xe8008015, "A valid provisioning profile for this executable was not found." }, + { 0xe8008016, "The executable was signed with invalid entitlements." }, + { 0xe8008017, "A signed resource has been added, modified, or deleted." }, + { 0xe8008018, "The identity used to sign the executable is no longer valid." }, + { 0xe8008019, "The application does not have a valid signature." }, + { 0xe800801a, "This provisioning profile does not have a valid signature (or it has a valid, but untrusted signature)." }, + { 0xe800801b, "There was an error communicating with your device." }, + { 0xe800801c, "No code signature found." }, + { 0xe800801d, "Rejected by policy." }, + { 0xe800801e, "The requested profile does not exist (it may have been removed)." }, + { 0xe800801f, "Attempted to install a Beta profile without the proper entitlement." }, + { 0xe8008020, "Attempted to install a Beta profile over lockdown connection." }, + { 0xe8008021, "The maximum number of apps for free development profiles has been reached." }, + { 0xe8008022, "An error occured while accessing the profile database." }, + { 0xe8008023, "An error occured while communicating with the agent." }, + { 0xe8008024, "The provisioning profile is banned." }, + { 0xe8008025, "The user did not explicitly trust the provisioning profile." }, + { 0xe8008026, "The provisioning profile requires online authorization." }, + { 0xe8008027, "The cdhash is not in the trust cache." }, + { 0xe8008028, "Invalid arguments or option combination." }, +}; + +const int errorcode_to_id_count = sizeof(errorcode_to_id) / sizeof(errorcode_to_id_t); + +// Taken from /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/English.lproj/Localizable.strings +error_id_to_message_t error_id_to_message[] = { + { "kAMDAPIInternalError", "There was an internal API error." }, + { "kAMDAlreadyArchivedError", "The application is already archived." }, + { "kAMDAppBlacklistedError", "This app is not allowed to be installed on this device." }, + { "kAMDAppexBundleIDConflictWithOtherIdentifierError", "This application contains an app extension with a bundle identifier that conflicts with the bundle identifier of another app or app extension already installed." }, + { "kAMDApplicationAlreadyInstalledError", "A system application with the given bundle identifier is already installed on the device and cannot be replaced." }, + { "kAMDApplicationMoveFailedError", "The application could not be moved into place on the device." }, + { "kAMDApplicationSandboxFailedError", "The application could not be sandboxed." }, + { "kAMDApplicationVerificationFailedError", "The application could not be verified." }, + { "kAMDArchiveDestructionFailedError", "Could not remove the application archive." }, + { "kAMDBadHeaderError", "Could not transfer file." }, + { "kAMDBreadcrumbFailedError", "Could not write installation breadcrumb." }, + { "kAMDBreadcrumbUnlockError", "Could not update installation breadcrumb." }, + { "kAMDBundleIDConflictWithOtherIdentifierError", "This application's bundle identifier conflicts with the identifier of another app or app extension already installed." }, + { "kAMDBundleVerificationFailedError", "The carrier bundle could not be verified." }, + { "kAMDBundleiTunesMetadataVersionMismatchError", "This application's iTunesMetadata.plist specifies versions that do not match the versions listed for the app in its Info.plist" }, + { "kAMDBusyError", "The device is busy." }, + { "kAMDCUPairingContinueError", "Continue pairing process over the network." }, + { "kAMDCUPairingError", "General failure while pairing over the network." }, + { "kAMDCUPairingResetError", "Pairing was reset due to earlier issues, try again." }, + { "kAMDCannotTranslateError", "Could not translate messages from device" }, + { "kAMDCarrierBundleCopyFailedError", "Could not install the carrier bundle." }, + { "kAMDCarrierBundleDirectoryCreationFailedError", "Could not create the carrier bundle directory." }, + { "kAMDCarrierBundleMissingSupportedSIMsError", "There are no supported SIMs for this carrier bundle." }, + { "kAMDCheckinConnectionFailedError", "The service did not start properly on the device." }, + { "kAMDCheckinOutOfMemoryError", "The service did not start properly on the device." }, + { "kAMDCheckinReceiveFailedError", "The service did not start properly on the device." }, + { "kAMDCheckinResponseFailedError", "The service did not start properly on the device." }, + { "kAMDCheckinSendFailedError", "The service did not start properly on the device." }, + { "kAMDCheckinSetupFailedError", "Could not start service on device" }, + { "kAMDCheckinTimeoutError", "The service did not start properly on the device." }, + { "kAMDCommCenterNotificationFailedError", "Could not listen for notification from the baseband." }, + { "kAMDContainerCreationFailedError", "Could not create application container." }, + { "kAMDContainerP0wnFailedError", "Could not repair permissions on application container." }, + { "kAMDContainerRemovalFailedError", "Could not remove the application container." }, + { "kAMDCryptoError", "Could not establish a secure connection to the device." }, + { "kAMDDeviceDisconnectedError", "This device is no longer connected." }, + { "kAMDDeviceFamilyNotSupported", "This application does not support this kind of device." }, + { "kAMDDeviceNotSupportedByThinningError", "This application is not built for this device." }, + { "kAMDDeviceOSVersionTooLow", "The device OS version is too low." }, + { "kAMDDeviceRefNoGood", "This device is no longer connected." }, + { "kAMDDeviceTooNewError", "This application needs to be updated." }, + { "kAMDDictCreationFailedError", "Could not extract capabilities from the request." }, + { "kAMDDigestFailedError", "Could not read disk image." }, + { "kAMDEOFError", "End of file." }, + { "kAMDEmbeddedProfileInstallFailedError", "Could not install the embedded provisioning profile." }, + { "kAMDErrorError", "An error occurred." }, + { "kAMDEscrowLockedError", "Device is not available until first unlock after boot." }, + { "kAMDExecutableTwiddleFailedError", "Could not change executable permissions on the application." }, + { "kAMDExistenceCheckFailedError", "Could not check to see if the application already exists." }, + { "kAMDFMiPProtectedError", "The device is in lost mode." }, + { "kAMDFileExistsError", "The file already exists." }, + { "kAMDGeoJSONCaptureFailedError", "Could not save the GeoJSON data." }, + { "kAMDGetProhibitedError", "Cannot retrieve value from the passcode locked device." }, + { "kAMDImmutableValueError", "This value cannot be changed." }, + { "kAMDIncorrectArchitectureError", "This application does not support this device's CPU type." }, + { "kAMDInstallMapUpdateFailedError", "Could not update the installed applications list." }, + { "kAMDInstallProhibitedError", "Installation of apps is prohibited by a policy on the device." }, + { "kAMDInvalidActivationRecordError", "The activation record is not valid." }, + { "kAMDInvalidArgumentError", "The argument is invalid." }, + { "kAMDInvalidCheckinError", "Could not start service on device" }, + { "kAMDInvalidDiskImageError", "The disk image is invalid." }, + { "kAMDInvalidHostIDError", "The device does not recognize this host." }, + { "kAMDInvalidPairRecordError", "The host is no longer paired with the device." }, + { "kAMDInvalidResponseError", "Received an unexpected response from the device." }, + { "kAMDInvalidSINFError", "The encryption information included with this application is not valid so this application cannot be installed on this device." }, + { "kAMDInvalidServiceError", "The service is invalid." }, + { "kAMDInvalidSessionIDError", "The session ID is invalid." }, + { "kAMDInvalidSymlinkError", "The bundle contained an invalid symlink." }, + { "kAMDInvalidiTunesMetadataPlistError", "This application's iTunesMetadata.plist is not valid." }, + { "kAMDIsDirectoryError", "The path is a directory." }, + { "kAMDLookupFailedError", "Could not list installed applications." }, + { "kAMDMCChallengeRequired", "A policy on the device requires secure pairing." }, + { "kAMDMCProtected", "Pairing is prohibited by a policy on the device." }, + { "kAMDManifestCaptureFailedError", "Could not save the application manifest." }, + { "kAMDMapGenerationFailedError", "Could not generate the map." }, + { "kAMDMismatchedApplicationIdentifierEntitlementError", "This application's application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed." }, + { "kAMDMismatchedBundleIDSigningIdentifierError", "This application's bundle identifier does not match its code signing identifier." }, + { "kAMDMissingActivationRecordError", "The activation record could not be found." }, + { "kAMDMissingApplicationIdentifierError", "Request was missing the application identifier." }, + { "kAMDMissingAttributeValueError", "Request was missing a required value." }, + { "kAMDMissingBundleExecutableError", "The application bundle does not contain an executable." }, + { "kAMDMissingBundleIdentifierError", "The application bundle does not contain a valid identifier." }, + { "kAMDMissingBundlePathError", "Could not determine the application bundle path." }, + { "kAMDMissingBundleVersionError", "The bundle's Info.plist does not contain a CFBundleVersion key or its value is not a string." }, + { "kAMDMissingCommandError", "The request did not contain a command." }, + { "kAMDMissingContainerError", "Could not find the container for the installed application." }, + { "kAMDMissingContainerPathError", "Request was missing the container path." }, + { "kAMDMissingDigestError", "The digest is missing." }, + { "kAMDMissingHostIDError", "The device does not recognize this host." }, + { "kAMDMissingImageTypeError", "The image is missing." }, + { "kAMDMissingKeyError", "The key is missing." }, + { "kAMDMissingOptionsError", "The options are missing." }, + { "kAMDMissingPackagePathError", "Request was missing the package path." }, + { "kAMDMissingPairRecordError", "The host is not paired with the device." }, + { "kAMDMissingServiceError", "The service is missing." }, + { "kAMDMissingSessionIDError", "The session ID is missing." }, + { "kAMDMissingValueError", "The value is missing." }, + { "kAMDMobileImageMounterAlreadyMounted", "Image is already mounted." }, + { "kAMDMobileImageMounterDeviceLocked", "The device is locked." }, + { "kAMDMobileImageMounterDigestCreationFailed", "Could not support development." }, + { "kAMDMobileImageMounterDigestFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageInfoCreationFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageMapLoadFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageMapStoreFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageMountFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageMoveFailed", "Could not support development." }, + { "kAMDMobileImageMounterImageVerificationFailed", "Could not support development." }, + { "kAMDMobileImageMounterMissingImagePath", "Could not support development." }, + { "kAMDMobileImageMounterMissingImageSignature", "Could not support development." }, + { "kAMDMobileImageMounterMissingImageType", "Could not support development." }, + { "kAMDMobileImageMounterMountPathMissing", "Could not support development." }, + { "kAMDMobileImageMounterMountPathNotEmpty", "Could not support development." }, + { "kAMDMobileImageMounterResponseCreationFailed", "Could not support development." }, + { "kAMDMobileImageMounterTrustCacheLoadFailed", "Could not support development." }, + { "kAMDMuxConnectError", "Could not connect to the device." }, + { "kAMDMuxCreateListenerError", "Could not listen for USB devices." }, + { "kAMDMuxError", "There was an error with the USB device multiplexor." }, + { "kAMDMuxGetListenerError", "Could not get the USB listener." }, + { "kAMDNewsstandArtworkCaptureFailedError", "Could not save the Newsstand artwork." }, + { "kAMDNoResourcesError", "Could not allocate a resource." }, + { "kAMDNoSpaceError", "No space is available on the device." }, + { "kAMDNoWifiSyncSupportError", "Device doesn't support wireless sync." }, + { "kAMDNotConnectedError", "Not connected to the device." }, + { "kAMDNotEntitledError", "The requesting application is not allowed to make this request." }, + { "kAMDNotFoundError", "The file could not be found." }, + { "kAMDNotificationFailedError", "Could not post a notification." }, + { "kAMDOverrunError", "There was a buffer overrun." }, + { "kAMDPackageExtractionFailedError", "Could not open the application package." }, + { "kAMDPackageInspectionFailedError", "Could not inspect the application package." }, + { "kAMDPackageMoveFailedError", "Could not move the application package into the staging location." }, + { "kAMDPackagePatchFailedError", "Could not apply patch update to application." }, + { "kAMDPairingDialogResponsePendingError", "The user has not yet responded to the pairing request." }, + { "kAMDPairingProhibitedError", "Pairing only allowed over USB." }, + { "kAMDPasswordProtectedError", "The device is passcode protected." }, + { "kAMDPasswordRequiredError", "A passcode is required to be set on the device." }, + { "kAMDPathConversionFailedError", "Could not convert the path." }, + { "kAMDPermissionError", "You do not have permission." }, + { "kAMDPluginCopyFailedError", "Could not copy VPN Plugin into app container." }, + { "kAMDProhibitedBySupervision", "Operation prohibited on supervised devices." }, + { "kAMDProvisioningProfileNotValid", "The provisioning profile is not valid." }, + { "kAMDReadError", "Could not read from the device." }, + { "kAMDReceiveMessageError", "Could not receive a message from the device." }, + { "kAMDRemoveProhibitedError", "Cannot remove value on device." }, + { "kAMDRequireCUPairingBackoffError", "Retry later." }, + { "kAMDRequireCUPairingCodeError", "Invalid PIN code entered." }, + { "kAMDRequireCUPairingError", "Cannot pair over network yet" }, + { "kAMDRestoreContainerFailedError", "Could not restore the application container." }, + { "kAMDSUFirmwareError", "Could not flash the firmware." }, + { "kAMDSUPatchError", "Could not patch the file." }, + { "kAMDSUVerificationError", "The software update package could not be verified." }, + { "kAMDSavePairRecordFailedError", "Could not save the pairing record." }, + { "kAMDSeatbeltProfileRemovalFailedError", "Could not remove the application seatbelt profile." }, + { "kAMDSendMessageError", "Could not send a message to the device." }, + { "kAMDServiceLimitError", "Too many instances of this service are already running." }, + { "kAMDServiceProhibitedError", "The service could not be started on the device." }, + { "kAMDSessionActiveError", "The session is active." }, + { "kAMDSessionInactiveError", "The session is inactive." }, + { "kAMDSetProhibitedError", "Cannot set value on device." }, + { "kAMDStageCreationFailedError", "Could not create the staging directory." }, + { "kAMDStartServiceError", "The service could not be started." }, + { "kAMDSuccess", "There was no error." }, + { "kAMDSymlinkFailedError", "Could not create the symlink." }, + { "kAMDTimeOutError", "The operation timed out." }, + { "kAMDTooBigError", "The message is too big." }, + { "kAMDUndefinedError", "An unknown error occurred." }, + { "kAMDUninstallProhibitedError", "Uninstallation of apps is prohibited by a policy on the device." }, + { "kAMDUnknownCommandError", "The device does not recognize the command." }, + { "kAMDUnknownPacketError", "The packet is unknown." }, + { "kAMDUnsupportedError", "This operation is unsupported." }, + { "kAMDUserDeniedPairingError", "The device rejected the pairing attempt." }, + { "kAMDWriteError", "Could not write to the device." }, + { "kAMDWrongDroidError", "The device is in recovery mode." }, + { "kAMDiTunesArtworkCaptureFailedError", "Could not save the iTunes artwork." }, + { "kAMDiTunesMetadataCaptureFailedError", "Could not save the iTunes metadata." }, + { "kMobileHouseArrestApplicationLookupFailed", "The requested application is not a user application." }, + { "kMobileHouseArrestDictionaryFailed", "The request contained an invalid request dictionary." }, + { "kMobileHouseArrestInstallationLookupFailed", "Could not find the requested application." }, + { "kMobileHouseArrestInvalidPath", "The requested application contained an invalid data container path." }, + { "kMobileHouseArrestMissingCommand", "The request was missing a command." }, + { "kMobileHouseArrestMissingContainer", "The requested application does not contain a valid data container." }, + { "kMobileHouseArrestMissingIdentifier", "The request was missing an application identifier." }, + { "kMobileHouseArrestPathConversionFailed", "Could not convert the requested application's data container path." }, + { "kMobileHouseArrestPathMissing", "The requested application's data container path does not exist." }, + { "kMobileHouseArrestUnknownCommand", "The request contained an invalid command." }, +}; + +const int error_id_to_message_count = sizeof(error_id_to_message) / sizeof(error_id_to_message_t); + +const char* get_error_message(unsigned int error) { + const char* id = NULL; + + // Lookup error localization id + for (int i = 0; i < errorcode_to_id_count; i++) { + if (errorcode_to_id[i].error == error) { + id = errorcode_to_id[i].id; + break; + } + } + + // Lookup error message + if (id) { + for (int i = 0; i < error_id_to_message_count; i++) + if (strcmp(error_id_to_message[i].id, id) == 0) + return error_id_to_message[i].message; + } + + // If message is not found, then at least return id if it was found, otherwise NULL + return id; +}; diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/ios-deploy.m b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/ios-deploy.m new file mode 100644 index 000000000000..37407219ac41 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/ios-deploy.m @@ -0,0 +1,2048 @@ +//TODO: don't copy/mount DeveloperDiskImage.dmg if it's already done - Xcode checks this somehow + +#import +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MobileDevice.h" +#import "errors.h" +#import "device_db.h" + +#define PREP_CMDS_PATH @"/tmp/%@/fruitstrap-lldb-prep-cmds-" +#define LLDB_SHELL @"lldb -s %@" +/* + * Startup script passed to lldb. + * To see how xcode interacts with lldb, put this into .lldbinit: + * log enable -v -f /Users/vargaz/lldb.log lldb all + * log enable -v -f /Users/vargaz/gdb-remote.log gdb-remote all + */ + +#define LLDB_PREP_CMDS CFSTR("\ + platform select remote-ios --sysroot '{symbols_path}'\n\ + target create \"{disk_app}\"\n\ + {disk_symbol_path}\ + script fruitstrap_device_app=\"{device_app}\"\n\ + script fruitstrap_connect_url=\"connect://127.0.0.1:{device_port}\"\n\ + target modules search-paths add {modules_search_paths_pairs}\n\ + command script import \"{python_file_path}\"\n\ + command script add -f {python_command}.connect_command connect\n\ + command script add -s asynchronous -f {python_command}.run_command run\n\ + command script add -s asynchronous -f {python_command}.autoexit_command autoexit\n\ + command script add -s asynchronous -f {python_command}.safequit_command safequit\n\ + connect\n\ +") + +const char* lldb_prep_no_cmds = ""; + +const char* lldb_prep_interactive_cmds = "\ + run\n\ +"; + +const char* lldb_prep_noninteractive_justlaunch_cmds = "\ + run\n\ + safequit\n\ +"; + +const char* lldb_prep_noninteractive_cmds = "\ + run\n\ + autoexit\n\ +"; + +/* + * Some things do not seem to work when using the normal commands like process connect/launch, so we invoke them + * through the python interface. Also, Launch () doesn't seem to work when ran from init_module (), so we add + * a command which can be used by the user to run it. + */ +NSString* LLDB_FRUITSTRAP_MODULE = @ + #include "lldb.py.h" +; + +typedef void (*iter_callback) (struct afc_connection *, char const *, char *); +void remove_path_recursively_conn(struct afc_connection *conn, char const *path); + +typedef struct am_device * AMDeviceRef; +mach_error_t AMDeviceSecureStartService(struct am_device *device, CFStringRef service_name, unsigned int *unknown, service_conn_t *handle); +int AMDeviceSecureTransferPath(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); +int AMDeviceSecureInstallApplication(int zero, AMDeviceRef device, CFURLRef url, CFDictionaryRef options, void *callback, int cbarg); +int AMDeviceMountImage(AMDeviceRef device, CFStringRef image, CFDictionaryRef options, void *callback, int cbarg); +mach_error_t AMDeviceLookupApplications(AMDeviceRef device, CFDictionaryRef options, CFDictionaryRef *result); +int AMDeviceGetInterfaceType(struct am_device *device); + +bool found_device = false, debug = false, verbose = false, unbuffered = false, nostart = false, debugserver_only = false, detect_only = false, install = true, uninstall = false, no_wifi = false; +bool command_only = false; +char *command = NULL; +char const*target_filename = NULL; +char const*upload_pathname = NULL; +char *bundle_id = NULL; +bool interactive = true; +bool justlaunch = false; +char *app_path = NULL; +char *disk_symbol_path = NULL; +char *device_id = NULL; +char *args = NULL; +char *list_root = NULL; +int _timeout = 0; +int _detectDeadlockTimeout = 0; +int port = 0; // 0 means "dynamically assigned" +CFStringRef last_path = NULL; +service_conn_t gdbfd; +pid_t parent = 0; +// PID of child process running lldb +pid_t child = 0; +// Signal sent from child to parent process when LLDB finishes. +const int SIGLLDB = SIGUSR1; +AMDeviceRef best_device_match = NULL; +NSString* tmpUUID; +struct am_device_notification *notify; + +// Error codes we report on different failures, so scripts can distinguish between user app exit +// codes and our exit codes. For non app errors we use codes in reserved 128-255 range. +const int exitcode_timeout = 252; +const int exitcode_error = 253; +const int exitcode_app_crash = 254; + +// Checks for MobileDevice.framework errors, tries to print them and exits. +#define check_error(call) \ + do { \ + unsigned int err = (unsigned int)call; \ + if (err != 0) \ + { \ + const char* msg = get_error_message(err); \ + /*on_error("Error 0x%x: %s " #call, err, msg ? msg : "unknown.");*/ \ + on_error(@"Error 0x%x: %@ " #call, err, msg ? [NSString stringWithUTF8String:msg] : @"unknown."); \ + } \ + } while (false); + +void on_error(NSString* format, ...) +{ + va_list valist; + va_start(valist, format); + NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; + va_end(valist); + + NSLog(@"[ !! ] %@", str); + + exit(exitcode_error); +} + +// Print error message getting last errno and exit +void on_sys_error(NSString* format, ...) { + const char* errstr = strerror(errno); + + va_list valist; + va_start(valist, format); + NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; + va_end(valist); + + on_error(@"%@ : %@", str, [NSString stringWithUTF8String:errstr]); +} + +void __NSLogOut(NSString* format, va_list valist) { + NSString* str = [[[NSString alloc] initWithFormat:format arguments:valist] autorelease]; + [[str stringByAppendingString:@"\n"] writeToFile:@"/dev/stdout" atomically:NO encoding:NSUTF8StringEncoding error:nil]; +} + +void NSLogOut(NSString* format, ...) { + va_list valist; + va_start(valist, format); + __NSLogOut(format, valist); + va_end(valist); +} + +void NSLogVerbose(NSString* format, ...) { + if (verbose) { + va_list valist; + va_start(valist, format); + __NSLogOut(format, valist); + va_end(valist); + } +} + + +BOOL mkdirp(NSString* path) { + NSError* error = nil; + BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:nil + error:&error]; + return success; +} + +Boolean path_exists(CFTypeRef path) { + if (CFGetTypeID(path) == CFStringGetTypeID()) { + CFURLRef url = CFURLCreateWithFileSystemPath(NULL, path, kCFURLPOSIXPathStyle, true); + Boolean result = CFURLResourceIsReachable(url, NULL); + CFRelease(url); + return result; + } else if (CFGetTypeID(path) == CFURLGetTypeID()) { + return CFURLResourceIsReachable(path, NULL); + } else { + return false; + } +} + +CFStringRef find_path(CFStringRef rootPath, CFStringRef namePattern) { + FILE *fpipe = NULL; + CFStringRef cf_command; + + if( !path_exists(rootPath) ) + return NULL; + + if (CFStringFind(namePattern, CFSTR("*"), 0).location == kCFNotFound) { + //No wildcards. Let's speed up the search + CFStringRef path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), rootPath, namePattern); + + if( path_exists(path) ) + return path; + + CFRelease(path); + return NULL; + } + + if (CFStringFind(namePattern, CFSTR("/"), 0).location == kCFNotFound) { + cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find '%@' -name '%@' -maxdepth 1 2>/dev/null | sort | tail -n 1"), rootPath, namePattern); + } else { + cf_command = CFStringCreateWithFormat(NULL, NULL, CFSTR("find '%@' -path '%@/%@' 2>/dev/null | sort | tail -n 1"), rootPath, rootPath, namePattern); + } + + char command[1024] = { '\0' }; + CFStringGetCString(cf_command, command, sizeof(command), kCFStringEncodingUTF8); + CFRelease(cf_command); + + if (!(fpipe = (FILE *)popen(command, "r"))) + on_sys_error(@"Error encountered while opening pipe"); + + char buffer[256] = { '\0' }; + + fgets(buffer, sizeof(buffer), fpipe); + pclose(fpipe); + + strtok(buffer, "\n"); + + CFStringRef path = CFStringCreateWithCString(NULL, buffer, kCFStringEncodingUTF8); + + if( CFStringGetLength(path) > 0 && path_exists(path) ) + return path; + + CFRelease(path); + return NULL; +} + +CFStringRef copy_xcode_dev_path() { + static char xcode_dev_path[256] = { '\0' }; + if (strlen(xcode_dev_path) == 0) { + FILE *fpipe = NULL; + char *command = "xcode-select -print-path"; + + if (!(fpipe = (FILE *)popen(command, "r"))) + on_sys_error(@"Error encountered while opening pipe"); + + char buffer[256] = { '\0' }; + + fgets(buffer, sizeof(buffer), fpipe); + pclose(fpipe); + + strtok(buffer, "\n"); + strcpy(xcode_dev_path, buffer); + } + return CFStringCreateWithCString(NULL, xcode_dev_path, kCFStringEncodingUTF8); +} + +const char *get_home() { + const char* home = getenv("HOME"); + if (!home) { + struct passwd *pwd = getpwuid(getuid()); + home = pwd->pw_dir; + } + return home; +} + +CFStringRef copy_xcode_path_for_impl(CFStringRef rootPath, CFStringRef subPath, CFStringRef search) { + CFStringRef searchPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), rootPath, subPath ); + CFStringRef res = find_path(searchPath, search); + CFRelease(searchPath); + return res; +} + +CFStringRef copy_xcode_path_for(CFStringRef subPath, CFStringRef search) { + CFStringRef xcodeDevPath = copy_xcode_dev_path(); + CFStringRef defaultXcodeDevPath = CFSTR("/Applications/Xcode.app/Contents/Developer"); + CFStringRef path = NULL; + const char* home = get_home(); + + // Try using xcode-select --print-path + path = copy_xcode_path_for_impl(xcodeDevPath, subPath, search); + + // If not look in the default xcode location (xcode-select is sometimes wrong) + if (path == NULL && CFStringCompare(xcodeDevPath, defaultXcodeDevPath, 0) != kCFCompareEqualTo ) + path = copy_xcode_path_for_impl(defaultXcodeDevPath, subPath, search); + + // If not look in the users home directory, Xcode can store device support stuff there + if (path == NULL) { + CFRelease(xcodeDevPath); + xcodeDevPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s/Library/Developer/Xcode"), home ); + path = copy_xcode_path_for_impl(xcodeDevPath, subPath, search); + } + + CFRelease(xcodeDevPath); + + return path; +} + +device_desc get_device_desc(CFStringRef model) { + if (model != NULL) { + size_t sz = sizeof(device_db) / sizeof(device_desc); + for (size_t i = 0; i < sz; i ++) { + if (CFStringCompare(model, device_db[i].model, kCFCompareNonliteral | kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + return device_db[i]; + } + } + } + + device_desc res = device_db[UNKNOWN_DEVICE_IDX]; + + res.model = model; + res.name = model; + + return res; +} + +char * MYCFStringCopyUTF8String(CFStringRef aString) { + if (aString == NULL) { + return NULL; + } + + CFIndex length = CFStringGetLength(aString); + CFIndex maxSize = + CFStringGetMaximumSizeForEncoding(length, + kCFStringEncodingUTF8); + char *buffer = (char *)malloc(maxSize); + if (CFStringGetCString(aString, buffer, maxSize, + kCFStringEncodingUTF8)) { + return buffer; + } + return NULL; +} + +CFStringRef get_device_full_name(const AMDeviceRef device) { + CFStringRef full_name = NULL, + device_udid = AMDeviceCopyDeviceIdentifier(device), + device_name = NULL, + model_name = NULL, + sdk_name = NULL, + arch_name = NULL; + + AMDeviceConnect(device); + + device_name = AMDeviceCopyValue(device, 0, CFSTR("DeviceName")); + + // Please ensure that device is connected or the name will be unknown + CFStringRef model = AMDeviceCopyValue(device, 0, CFSTR("HardwareModel")); + device_desc dev; + if (model != NULL) { + dev = get_device_desc(model); + } else { + dev= device_db[UNKNOWN_DEVICE_IDX]; + model = dev.model; + } + model_name = dev.name; + sdk_name = dev.sdk; + arch_name = dev.arch; + + NSLogVerbose(@"Hardware Model: %@", model); + NSLogVerbose(@"Device Name: %@", device_name); + NSLogVerbose(@"Model Name: %@", model_name); + NSLogVerbose(@"SDK Name: %@", sdk_name); + NSLogVerbose(@"Architecture Name: %@", arch_name); + + if (device_name != NULL) { + full_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@, %@, %@, %@) a.k.a. '%@'"), device_udid, model, model_name, sdk_name, arch_name, device_name); + } else { + full_name = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@, %@, %@, %@)"), device_udid, model, model_name, sdk_name, arch_name); + } + + AMDeviceDisconnect(device); + + if(device_udid != NULL) + CFRelease(device_udid); + if(device_name != NULL) + CFRelease(device_name); + if(model_name != NULL) + CFRelease(model_name); + + return full_name; +} + +CFStringRef get_device_interface_name(const AMDeviceRef device) { + // AMDeviceGetInterfaceType(device) 0=Unknown, 1 = Direct/USB, 2 = Indirect/WIFI + switch(AMDeviceGetInterfaceType(device)) { + case 1: + return CFSTR("USB"); + case 2: + return CFSTR("WIFI"); + default: + return CFSTR("Unknown Connection"); + } +} + +CFMutableArrayRef get_device_product_version_parts(AMDeviceRef device) { + CFStringRef version = AMDeviceCopyValue(device, 0, CFSTR("ProductVersion")); + CFArrayRef parts = CFStringCreateArrayBySeparatingStrings(NULL, version, CFSTR(".")); + CFMutableArrayRef result = CFArrayCreateMutableCopy(NULL, CFArrayGetCount(parts), parts); + CFRelease(version); + CFRelease(parts); + return result; +} + +CFStringRef copy_device_support_path(AMDeviceRef device, CFStringRef suffix) { + time_t startTime, endTime; + time( &startTime ); + + CFStringRef version = NULL; + CFStringRef build = AMDeviceCopyValue(device, 0, CFSTR("BuildVersion")); + CFStringRef deviceClass = AMDeviceCopyValue(device, 0, CFSTR("DeviceClass")); + CFStringRef path = NULL; + CFMutableArrayRef version_parts = get_device_product_version_parts(device); + + NSLogVerbose(@"Device Class: %@", deviceClass); + NSLogVerbose(@"build: %@", build); + + CFStringRef deviceClassPath[2]; + + if (CFStringCompare(CFSTR("AppleTV"), deviceClass, 0) == kCFCompareEqualTo) { + deviceClassPath[0] = CFSTR("Platforms/AppleTVOS.platform/DeviceSupport"); + deviceClassPath[1] = CFSTR("tvOS DeviceSupport"); + } else { + deviceClassPath[0] = CFSTR("Platforms/iPhoneOS.platform/DeviceSupport"); + deviceClassPath[1] = CFSTR("iOS DeviceSupport"); + } + while (CFArrayGetCount(version_parts) > 0) { + version = CFStringCreateByCombiningStrings(NULL, version_parts, CFSTR(".")); + NSLogVerbose(@"version: %@", version); + + for( int i = 0; i < 2; ++i ) { + if (path == NULL) { + path = copy_xcode_path_for(deviceClassPath[i], CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (%@)/%@"), version, build, suffix)); + } + + if (path == NULL) { + path = copy_xcode_path_for(deviceClassPath[i], CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ (*)/%@"), version, suffix)); + } + + if (path == NULL) { + path = copy_xcode_path_for(deviceClassPath[i], CFStringCreateWithFormat(NULL, NULL, CFSTR("%@/%@"), version, suffix)); + } + + if (path == NULL) { + path = copy_xcode_path_for(deviceClassPath[i], CFStringCreateWithFormat(NULL, NULL, CFSTR("%@.*/%@"), version, suffix)); + } + } + + CFRelease(version); + if (path != NULL) { + break; + } + CFArrayRemoveValueAtIndex(version_parts, CFArrayGetCount(version_parts) - 1); + } + + for( int i = 0; i < 2; ++i ) { + if (path == NULL) { + path = copy_xcode_path_for(deviceClassPath[i], CFStringCreateWithFormat(NULL, NULL, CFSTR("Latest/%@"), suffix)); + } + } + + CFRelease(version_parts); + CFRelease(build); + CFRelease(deviceClass); + if (path == NULL) + on_error([NSString stringWithFormat:@"Unable to locate DeviceSupport directory with suffix '%@'. This probably means you don't have Xcode installed, you will need to launch the app manually and logging output will not be shown!", suffix]); + + time( &endTime ); + NSLogVerbose(@"DeviceSupport directory '%@' was located. It took %.2f seconds", path, difftime(endTime,startTime)); + + return path; +} + +void mount_callback(CFDictionaryRef dict, int arg) { + CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); + + if (CFEqual(status, CFSTR("LookingUpImage"))) { + NSLogOut(@"[ 0%%] Looking up developer disk image"); + } else if (CFEqual(status, CFSTR("CopyingImage"))) { + NSLogOut(@"[ 30%%] Copying DeveloperDiskImage.dmg to device"); + } else if (CFEqual(status, CFSTR("MountingImage"))) { + NSLogOut(@"[ 90%%] Mounting developer disk image"); + } +} + +void mount_developer_image(AMDeviceRef device) { + CFStringRef image_path = copy_device_support_path(device, CFSTR("DeveloperDiskImage.dmg")); + CFStringRef sig_path = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@.signature"), image_path); + + NSLogVerbose(@"Developer disk image: %@", image_path); + + FILE* sig = fopen(CFStringGetCStringPtr(sig_path, kCFStringEncodingMacRoman), "rb"); + void *sig_buf = malloc(128); + assert(fread(sig_buf, 1, 128, sig) == 128); + fclose(sig); + CFDataRef sig_data = CFDataCreateWithBytesNoCopy(NULL, sig_buf, 128, NULL); + CFRelease(sig_path); + + CFTypeRef keys[] = { CFSTR("ImageSignature"), CFSTR("ImageType") }; + CFTypeRef values[] = { sig_data, CFSTR("Developer") }; + CFDictionaryRef options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFRelease(sig_data); + + int result = AMDeviceMountImage(device, image_path, options, &mount_callback, 0); + if (result == 0) { + NSLogOut(@"[ 95%%] Developer disk image mounted successfully"); + } else if (result == 0xe8000076 /* already mounted */) { + NSLogOut(@"[ 95%%] Developer disk image already mounted"); + } else { + on_error(@"Unable to mount developer disk image. (%x)", result); + } + + CFRelease(image_path); + CFRelease(options); +} + +mach_error_t transfer_callback(CFDictionaryRef dict, int arg) { + int percent; + CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); + CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); + + if (CFEqual(status, CFSTR("CopyingFile"))) { + CFStringRef path = CFDictionaryGetValue(dict, CFSTR("Path")); + + if ((last_path == NULL || !CFEqual(path, last_path)) && !CFStringHasSuffix(path, CFSTR(".ipa"))) { + NSLogOut(@"[%3d%%] Copying %@ to device", percent / 2, path); + } + + if (last_path != NULL) { + CFRelease(last_path); + } + last_path = CFStringCreateCopy(NULL, path); + } + + return 0; +} + +mach_error_t install_callback(CFDictionaryRef dict, int arg) { + int percent; + CFStringRef status = CFDictionaryGetValue(dict, CFSTR("Status")); + CFNumberGetValue(CFDictionaryGetValue(dict, CFSTR("PercentComplete")), kCFNumberSInt32Type, &percent); + + NSLogOut(@"[%3d%%] %@", (percent / 2) + 50, status); + return 0; +} + +CFURLRef copy_device_app_url(AMDeviceRef device, CFStringRef identifier) { + CFDictionaryRef result = nil; + + NSArray *a = [NSArray arrayWithObjects: + @"CFBundleIdentifier", // absolute must + @"ApplicationDSID", + @"ApplicationType", + @"CFBundleExecutable", + @"CFBundleDisplayName", + @"CFBundleIconFile", + @"CFBundleName", + @"CFBundleShortVersionString", + @"CFBundleSupportedPlatforms", + @"CFBundleURLTypes", + @"CodeInfoIdentifier", + @"Container", + @"Entitlements", + @"HasSettingsBundle", + @"IsUpgradeable", + @"MinimumOSVersion", + @"Path", + @"SignerIdentity", + @"UIDeviceFamily", + @"UIFileSharingEnabled", + @"UIStatusBarHidden", + @"UISupportedInterfaceOrientations", + nil]; + + NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; + CFDictionaryRef options = (CFDictionaryRef)optionsDict; + + check_error(AMDeviceLookupApplications(device, options, &result)); + + CFDictionaryRef app_dict = CFDictionaryGetValue(result, identifier); + assert(app_dict != NULL); + + CFStringRef app_path = CFDictionaryGetValue(app_dict, CFSTR("Path")); + assert(app_path != NULL); + + CFURLRef url = CFURLCreateWithFileSystemPath(NULL, app_path, kCFURLPOSIXPathStyle, true); + CFRelease(result); + return url; +} + +CFStringRef copy_disk_app_identifier(CFURLRef disk_app_url) { + CFURLRef plist_url = CFURLCreateCopyAppendingPathComponent(NULL, disk_app_url, CFSTR("Info.plist"), false); + CFReadStreamRef plist_stream = CFReadStreamCreateWithFile(NULL, plist_url); + if (!CFReadStreamOpen(plist_stream)) { + on_error(@"Cannot read Info.plist file: %@", plist_url); + } + + CFPropertyListRef plist = CFPropertyListCreateWithStream(NULL, plist_stream, 0, kCFPropertyListImmutable, NULL, NULL); + CFStringRef bundle_identifier = CFRetain(CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier"))); + CFReadStreamClose(plist_stream); + + CFRelease(plist_url); + CFRelease(plist_stream); + CFRelease(plist); + + return bundle_identifier; +} + +CFStringRef copy_modules_search_paths_pairs(CFStringRef symbols_path, CFStringRef disk_container, CFStringRef device_container_private, CFStringRef device_container_noprivate ) +{ + CFMutableStringRef res = CFStringCreateMutable(kCFAllocatorDefault, 0); + CFStringAppendFormat(res, NULL, CFSTR("/usr \"%@/usr\""), symbols_path); + CFStringAppendFormat(res, NULL, CFSTR(" /System \"%@/System\""), symbols_path); + + CFStringAppendFormat(res, NULL, CFSTR(" \"%@\" \"%@\""), device_container_private, disk_container); + CFStringAppendFormat(res, NULL, CFSTR(" \"%@\" \"%@\""), device_container_noprivate, disk_container); + CFStringAppendFormat(res, NULL, CFSTR(" /Developer \"%@/Developer\""), symbols_path); + + return res; +} + +void write_lldb_prep_cmds(AMDeviceRef device, CFURLRef disk_app_url) { + CFStringRef symbols_path = copy_device_support_path(device, CFSTR("Symbols")); + CFMutableStringRef cmds = CFStringCreateMutableCopy(NULL, 0, LLDB_PREP_CMDS); + CFRange range = { 0, CFStringGetLength(cmds) }; + + CFStringFindAndReplace(cmds, CFSTR("{symbols_path}"), symbols_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFMutableStringRef pmodule = CFStringCreateMutableCopy(NULL, 0, (CFStringRef)LLDB_FRUITSTRAP_MODULE); + + CFRange rangeLLDB = { 0, CFStringGetLength(pmodule) }; + + CFStringRef exitcode_app_crash_str = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), exitcode_app_crash); + CFStringFindAndReplace(pmodule, CFSTR("{exitcode_app_crash}"), exitcode_app_crash_str, rangeLLDB, 0); + rangeLLDB.length = CFStringGetLength(pmodule); + + CFStringRef detect_deadlock_timeout_str = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), _detectDeadlockTimeout); + CFStringFindAndReplace(pmodule, CFSTR("{detect_deadlock_timeout}"), detect_deadlock_timeout_str, rangeLLDB, 0); + rangeLLDB.length = CFStringGetLength(pmodule); + + if (args) { + CFStringRef cf_args = CFStringCreateWithCString(NULL, args, kCFStringEncodingUTF8); + CFStringFindAndReplace(cmds, CFSTR("{args}"), cf_args, range, 0); + rangeLLDB.length = CFStringGetLength(pmodule); + CFStringFindAndReplace(pmodule, CFSTR("{args}"), cf_args, rangeLLDB, 0); + + //printf("write_lldb_prep_cmds:args: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), + // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); + CFRelease(cf_args); + } else { + CFStringFindAndReplace(cmds, CFSTR("{args}"), CFSTR(""), range, 0); + CFStringFindAndReplace(pmodule, CFSTR("{args}"), CFSTR(""), rangeLLDB, 0); + //printf("write_lldb_prep_cmds: [%s][%s]\n", CFStringGetCStringPtr (cmds,kCFStringEncodingMacRoman), + // CFStringGetCStringPtr(pmodule, kCFStringEncodingMacRoman)); + } + range.length = CFStringGetLength(cmds); + + CFStringRef bundle_identifier = copy_disk_app_identifier(disk_app_url); + CFURLRef device_app_url = copy_device_app_url(device, bundle_identifier); + CFStringRef device_app_path = CFURLCopyFileSystemPath(device_app_url, kCFURLPOSIXPathStyle); + CFStringFindAndReplace(cmds, CFSTR("{device_app}"), device_app_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFStringRef disk_app_path = CFURLCopyFileSystemPath(disk_app_url, kCFURLPOSIXPathStyle); + CFStringFindAndReplace(cmds, CFSTR("{disk_app}"), disk_app_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFStringRef cf_disk_symbol_path; + if (!disk_symbol_path) + { + cf_disk_symbol_path = CFStringCreateWithCString(NULL, "", kCFStringEncodingUTF8); + + } + else + { + cf_disk_symbol_path = CFStringCreateWithFormat(NULL, NULL, CFSTR("add-dsym \"%s\"\n"), disk_symbol_path); + } + + CFStringFindAndReplace(cmds, CFSTR("{disk_symbol_path}"), cf_disk_symbol_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFStringRef device_port = CFStringCreateWithFormat(NULL, NULL, CFSTR("%d"), port); + CFStringFindAndReplace(cmds, CFSTR("{device_port}"), device_port, range, 0); + range.length = CFStringGetLength(cmds); + + CFURLRef device_container_url = CFURLCreateCopyDeletingLastPathComponent(NULL, device_app_url); + CFStringRef device_container_path = CFURLCopyFileSystemPath(device_container_url, kCFURLPOSIXPathStyle); + CFMutableStringRef dcp_noprivate = CFStringCreateMutableCopy(NULL, 0, device_container_path); + range.length = CFStringGetLength(dcp_noprivate); + CFStringFindAndReplace(dcp_noprivate, CFSTR("/private/var/"), CFSTR("/var/"), range, 0); + range.length = CFStringGetLength(cmds); + CFStringFindAndReplace(cmds, CFSTR("{device_container}"), dcp_noprivate, range, 0); + range.length = CFStringGetLength(cmds); + + CFURLRef disk_container_url = CFURLCreateCopyDeletingLastPathComponent(NULL, disk_app_url); + CFStringRef disk_container_path = CFURLCopyFileSystemPath(disk_container_url, kCFURLPOSIXPathStyle); + CFStringFindAndReplace(cmds, CFSTR("{disk_container}"), disk_container_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFStringRef search_paths_pairs = copy_modules_search_paths_pairs(symbols_path, disk_container_path, device_container_path, dcp_noprivate); + CFStringFindAndReplace(cmds, CFSTR("{modules_search_paths_pairs}"), search_paths_pairs, range, 0); + range.length = CFStringGetLength(cmds); + CFRelease(search_paths_pairs); + + NSString* python_file_path = [NSString stringWithFormat:@"/tmp/%@/fruitstrap_", tmpUUID]; + mkdirp(python_file_path); + + NSString* python_command = @"fruitstrap_"; + if(device_id != NULL) { + python_file_path = [python_file_path stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; + python_command = [python_command stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; + } + python_file_path = [python_file_path stringByAppendingString:@".py"]; + + CFStringFindAndReplace(cmds, CFSTR("{python_command}"), (CFStringRef)python_command, range, 0); + range.length = CFStringGetLength(cmds); + CFStringFindAndReplace(cmds, CFSTR("{python_file_path}"), (CFStringRef)python_file_path, range, 0); + range.length = CFStringGetLength(cmds); + + CFDataRef cmds_data = CFStringCreateExternalRepresentation(NULL, cmds, kCFStringEncodingUTF8, 0); + NSString* prep_cmds_path = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; + if(device_id != NULL) { + prep_cmds_path = [prep_cmds_path stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; + } + FILE *out = fopen([prep_cmds_path UTF8String], "w"); + fwrite(CFDataGetBytePtr(cmds_data), CFDataGetLength(cmds_data), 1, out); + // Write additional commands based on mode we're running in + const char* extra_cmds; + if (!interactive) + { + if (justlaunch) + extra_cmds = lldb_prep_noninteractive_justlaunch_cmds; + else + extra_cmds = lldb_prep_noninteractive_cmds; + } + else if (nostart) + extra_cmds = lldb_prep_no_cmds; + else + extra_cmds = lldb_prep_interactive_cmds; + fwrite(extra_cmds, strlen(extra_cmds), 1, out); + fclose(out); + + CFDataRef pmodule_data = CFStringCreateExternalRepresentation(NULL, pmodule, kCFStringEncodingUTF8, 0); + + out = fopen([python_file_path UTF8String], "w"); + fwrite(CFDataGetBytePtr(pmodule_data), CFDataGetLength(pmodule_data), 1, out); + fclose(out); + + CFRelease(cmds); + CFRelease(symbols_path); + CFRelease(bundle_identifier); + CFRelease(device_app_url); + CFRelease(device_app_path); + CFRelease(cf_disk_symbol_path); + CFRelease(disk_app_path); + CFRelease(device_container_url); + CFRelease(device_container_path); + CFRelease(dcp_noprivate); + CFRelease(disk_container_url); + CFRelease(disk_container_path); + CFRelease(cmds_data); +} + +CFSocketRef server_socket; +CFSocketRef lldb_socket; +CFWriteStreamRef serverWriteStream = NULL; +CFWriteStreamRef lldbWriteStream = NULL; + +int kill_ptree(pid_t root, int signum); +void +server_callback (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) +{ + ssize_t res; + + if (CFDataGetLength (data) == 0) { + // close the socket on which we've got end-of-file, the server_socket. + CFSocketInvalidate(s); + CFRelease(s); + return; + } + res = write (CFSocketGetNative (lldb_socket), CFDataGetBytePtr (data), CFDataGetLength (data)); +} + +void lldb_callback(CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) +{ + //printf ("lldb: %s\n", CFDataGetBytePtr (data)); + + if (CFDataGetLength (data) == 0) { + // close the socket on which we've got end-of-file, the lldb_socket. + CFSocketInvalidate(s); + CFRelease(s); + return; + } + write (gdbfd, CFDataGetBytePtr (data), CFDataGetLength (data)); +} + +void fdvendor_callback(CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info) { + CFSocketNativeHandle socket = (CFSocketNativeHandle)(*((CFSocketNativeHandle *)data)); + + assert (callbackType == kCFSocketAcceptCallBack); + //PRINT ("callback!\n"); + + lldb_socket = CFSocketCreateWithNative(NULL, socket, kCFSocketDataCallBack, &lldb_callback, NULL); + int flag = 1; + int res = setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(flag)); + assert(res == 0); + CFRunLoopAddSource(CFRunLoopGetMain(), CFSocketCreateRunLoopSource(NULL, lldb_socket, 0), kCFRunLoopCommonModes); + + CFSocketInvalidate(s); + CFRelease(s); +} + +void start_remote_debug_server(AMDeviceRef device) { + + check_error(AMDeviceStartService(device, CFSTR("com.apple.debugserver"), &gdbfd, NULL)); + assert(gdbfd > 0); + + /* + * The debugserver connection is through a fd handle, while lldb requires a host/port to connect, so create an intermediate + * socket to transfer data. + */ + server_socket = CFSocketCreateWithNative (NULL, gdbfd, kCFSocketDataCallBack, &server_callback, NULL); + CFRunLoopAddSource(CFRunLoopGetMain(), CFSocketCreateRunLoopSource(NULL, server_socket, 0), kCFRunLoopCommonModes); + + struct sockaddr_in addr4; + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_len = sizeof(addr4); + addr4.sin_family = AF_INET; + addr4.sin_port = htons(port); + addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + CFSocketRef fdvendor = CFSocketCreate(NULL, PF_INET, 0, 0, kCFSocketAcceptCallBack, &fdvendor_callback, NULL); + + if (port) { + int yes = 1; + setsockopt(CFSocketGetNative(fdvendor), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + } + + CFDataRef address_data = CFDataCreate(NULL, (const UInt8 *)&addr4, sizeof(addr4)); + + CFSocketSetAddress(fdvendor, address_data); + CFRelease(address_data); + socklen_t addrlen = sizeof(addr4); + int res = getsockname(CFSocketGetNative(fdvendor),(struct sockaddr *)&addr4,&addrlen); + assert(res == 0); + port = ntohs(addr4.sin_port); + + CFRunLoopAddSource(CFRunLoopGetMain(), CFSocketCreateRunLoopSource(NULL, fdvendor, 0), kCFRunLoopCommonModes); +} + +void kill_ptree_inner(pid_t root, int signum, struct kinfo_proc *kp, int kp_len) { + int i; + for (i = 0; i < kp_len; i++) { + if (kp[i].kp_eproc.e_ppid == root) { + kill_ptree_inner(kp[i].kp_proc.p_pid, signum, kp, kp_len); + } + } + if (root != getpid()) { + kill(root, signum); + } +} + +int kill_ptree(pid_t root, int signum) { + int mib[3]; + size_t len; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_ALL; + if (sysctl(mib, 3, NULL, &len, NULL, 0) == -1) { + return -1; + } + + struct kinfo_proc *kp = calloc(1, len); + if (!kp) { + return -1; + } + + if (sysctl(mib, 3, kp, &len, NULL, 0) == -1) { + free(kp); + return -1; + } + + kill_ptree_inner(root, signum, kp, (int)(len / sizeof(struct kinfo_proc))); + + free(kp); + return 0; +} + +void killed(int signum) { + // SIGKILL needed to kill lldb, probably a better way to do this. + kill(0, SIGKILL); + _exit(0); +} + +void lldb_finished_handler(int signum) +{ + int status = 0; + if (waitpid(child, &status, 0) == -1) + perror("waitpid failed"); + _exit(WEXITSTATUS(status)); +} + +void bring_process_to_foreground() { + if (setpgid(0, 0) == -1) + perror("setpgid failed"); + + signal(SIGTTOU, SIG_IGN); + if (tcsetpgrp(STDIN_FILENO, getpid()) == -1) + perror("tcsetpgrp failed"); + signal(SIGTTOU, SIG_DFL); +} + +void setup_dummy_pipe_on_stdin(int pfd[2]) { + if (pipe(pfd) == -1) + perror("pipe failed"); + if (dup2(pfd[0], STDIN_FILENO) == -1) + perror("dup2 failed"); +} + +void setup_lldb(AMDeviceRef device, CFURLRef url) { + CFStringRef device_full_name = get_device_full_name(device), + device_interface_name = get_device_interface_name(device); + + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + NSLogOut(@"------ Debug phase ------"); + + if(AMDeviceGetInterfaceType(device) == 2) + { + NSLogOut(@"Cannot debug %@ over %@.", device_full_name, device_interface_name); + exit(0); + } + + NSLogOut(@"Starting debug of %@ connected through %@...", device_full_name, device_interface_name); + + mount_developer_image(device); // put debugserver on the device + start_remote_debug_server(device); // start debugserver + if (!debugserver_only) + write_lldb_prep_cmds(device, url); // dump the necessary lldb commands into a file + + CFRelease(url); + + NSLogOut(@"[100%%] Connecting to remote debug server"); + NSLogOut(@"-------------------------"); + + setpgid(getpid(), 0); + signal(SIGHUP, killed); + signal(SIGINT, killed); + signal(SIGTERM, killed); + // Need this before fork to avoid race conditions. For child process we remove this right after fork. + signal(SIGLLDB, lldb_finished_handler); + + parent = getpid(); +} + +void launch_debugger(AMDeviceRef device, CFURLRef url) { + setup_lldb(device, url); + int pid = fork(); + if (pid == 0) { + signal(SIGHUP, SIG_DFL); + signal(SIGLLDB, SIG_DFL); + child = getpid(); + + int pfd[2] = {-1, -1}; + if (isatty(STDIN_FILENO)) + // If we are running on a terminal, then we need to bring process to foreground for input + // to work correctly on lldb's end. + bring_process_to_foreground(); + else + // If lldb is running in a non terminal environment, then it freaks out spamming "^D" and + // "quit". It seems this is caused by read() on stdin returning EOF in lldb. To hack around + // this we setup a dummy pipe on stdin, so read() would block expecting "user's" input. + setup_dummy_pipe_on_stdin(pfd); + + NSString* lldb_shell; + NSString* prep_cmds = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; + lldb_shell = [NSString stringWithFormat:LLDB_SHELL, prep_cmds]; + + if(device_id != NULL) { + lldb_shell = [lldb_shell stringByAppendingString: [[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; + } + + int status = system([lldb_shell UTF8String]); // launch lldb + if (status == -1) + perror("failed launching lldb"); + + close(pfd[0]); + close(pfd[1]); + + // Notify parent we're exiting + kill(parent, SIGLLDB); + // Pass lldb exit code + _exit(WEXITSTATUS(status)); + } else if (pid > 0) { + child = pid; + } else { + on_sys_error(@"Fork failed"); + } +} + +void launch_debugger_and_exit(AMDeviceRef device, CFURLRef url) { + setup_lldb(device,url); + int pfd[2] = {-1, -1}; + if (pipe(pfd) == -1) + perror("Pipe failed"); + int pid = fork(); + if (pid == 0) { + signal(SIGHUP, SIG_DFL); + signal(SIGLLDB, SIG_DFL); + child = getpid(); + + if (dup2(pfd[0],STDIN_FILENO) == -1) + perror("dup2 failed"); + + + NSString* prep_cmds = [NSString stringWithFormat:PREP_CMDS_PATH, tmpUUID]; + NSString* lldb_shell = [NSString stringWithFormat:LLDB_SHELL, prep_cmds]; + if(device_id != NULL) { + lldb_shell = [lldb_shell stringByAppendingString:[[NSString stringWithUTF8String:device_id] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]]; + } + + int status = system([lldb_shell UTF8String]); // launch lldb + if (status == -1) + perror("failed launching lldb"); + + close(pfd[0]); + + // Notify parent we're exiting + kill(parent, SIGLLDB); + // Pass lldb exit code + _exit(WEXITSTATUS(status)); + } else if (pid > 0) { + child = pid; + NSLogVerbose(@"Waiting for child [Child: %d][Parent: %d]\n", child, parent); + } else { + on_sys_error(@"Fork failed"); + } +} + +void launch_debugserver_only(AMDeviceRef device, CFURLRef url) +{ + CFRetain(url); + setup_lldb(device,url); + + CFStringRef bundle_identifier = copy_disk_app_identifier(url); + CFURLRef device_app_url = copy_device_app_url(device, bundle_identifier); + CFStringRef device_app_path = CFURLCopyFileSystemPath(device_app_url, kCFURLPOSIXPathStyle); + CFRelease(url); + + NSLogOut(@"debugserver port: %d", port); + NSLogOut(@"App path: %@", device_app_path); +} + +CFStringRef get_bundle_id(CFURLRef app_url) +{ + if (app_url == NULL) + return NULL; + + CFURLRef url = CFURLCreateCopyAppendingPathComponent(NULL, app_url, CFSTR("Info.plist"), false); + + if (url == NULL) + return NULL; + + CFReadStreamRef stream = CFReadStreamCreateWithFile(NULL, url); + CFRelease(url); + + if (stream == NULL) + return NULL; + + CFPropertyListRef plist = NULL; + if (CFReadStreamOpen(stream) == TRUE) { + plist = CFPropertyListCreateWithStream(NULL, stream, 0, + kCFPropertyListImmutable, NULL, NULL); + } + CFReadStreamClose(stream); + CFRelease(stream); + + if (plist == NULL) + return NULL; + + const void *value = CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier")); + CFStringRef bundle_id = NULL; + if (value != NULL) + bundle_id = CFRetain(value); + + CFRelease(plist); + return bundle_id; +} + + +void read_dir(service_conn_t afcFd, afc_connection* afc_conn_p, const char* dir, + void(*callback)(afc_connection *conn,const char *dir,int file)) +{ + char *dir_ent; + + afc_connection afc_conn; + if (!afc_conn_p) { + afc_conn_p = &afc_conn; + AFCConnectionOpen(afcFd, 0, &afc_conn_p); + } + + afc_dictionary* afc_dict_p; + char *key, *val; + int not_dir = 0; + + unsigned int code = AFCFileInfoOpen(afc_conn_p, dir, &afc_dict_p); + if (code != 0) { + // there was a problem reading or opening the file to get info on it, abort + return; + } + + while((AFCKeyValueRead(afc_dict_p,&key,&val) == 0) && key && val) { + if (strcmp(key,"st_ifmt")==0) { + not_dir = strcmp(val,"S_IFDIR"); + break; + } + } + AFCKeyValueClose(afc_dict_p); + + if (not_dir) { + NSLogOut(@"%@", [NSString stringWithUTF8String:dir]); + } else { + NSLogOut(@"%@/", [NSString stringWithUTF8String:dir]); + } + + if (not_dir) { + if (callback) (*callback)(afc_conn_p, dir, not_dir); + return; + } + + afc_directory* afc_dir_p; + afc_error_t err = AFCDirectoryOpen(afc_conn_p, dir, &afc_dir_p); + + if (err != 0) { + // Couldn't open dir - was probably a file + return; + } else { + if (callback) (*callback)(afc_conn_p, dir, not_dir); + } + + while(true) { + err = AFCDirectoryRead(afc_conn_p, afc_dir_p, &dir_ent); + + if (err != 0 || !dir_ent) + break; + + if (strcmp(dir_ent, ".") == 0 || strcmp(dir_ent, "..") == 0) + continue; + + char* dir_joined = malloc(strlen(dir) + strlen(dir_ent) + 2); + strcpy(dir_joined, dir); + if (dir_joined[strlen(dir)-1] != '/') + strcat(dir_joined, "/"); + strcat(dir_joined, dir_ent); + read_dir(afcFd, afc_conn_p, dir_joined, callback); + free(dir_joined); + } + + AFCDirectoryClose(afc_conn_p, afc_dir_p); +} + + +// Used to send files to app-specific sandbox (Documents dir) +service_conn_t start_house_arrest_service(AMDeviceRef device) { + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + service_conn_t houseFd; + + if (bundle_id == NULL) { + on_error(@"Bundle id is not specified"); + } + + CFStringRef cf_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); + if (AMDeviceStartHouseArrestService(device, cf_bundle_id, 0, &houseFd, 0) != 0) + { + on_error(@"Unable to find bundle with id: %@", [NSString stringWithUTF8String:bundle_id]); + } + + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + CFRelease(cf_bundle_id); + + return houseFd; +} + +char const* get_filename_from_path(char const* path) +{ + char const*ptr = path + strlen(path); + while (ptr > path) + { + if (*ptr == '/') + break; + --ptr; + } + if (ptr+1 >= path+strlen(path)) + return NULL; + if (ptr == path) + return ptr; + return ptr+1; +} + +void* read_file_to_memory(char const * path, size_t* file_size) +{ + struct stat buf; + int err = stat(path, &buf); + if (err < 0) + { + return NULL; + } + + *file_size = buf.st_size; + FILE* fd = fopen(path, "r"); + char* content = malloc(*file_size); + if (*file_size != 0 && fread(content, *file_size, 1, fd) != 1) + { + fclose(fd); + return NULL; + } + fclose(fd); + return content; +} + +void list_files(AMDeviceRef device) +{ + service_conn_t houseFd = start_house_arrest_service(device); + + afc_connection* afc_conn_p; + if (AFCConnectionOpen(houseFd, 0, &afc_conn_p) == 0) { + read_dir(houseFd, afc_conn_p, list_root?list_root:"/", NULL); + AFCConnectionClose(afc_conn_p); + } +} + +int app_exists(AMDeviceRef device) +{ + if (bundle_id == NULL) { + NSLogOut(@"Bundle id is not specified."); + return 1; + } + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + CFStringRef cf_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); + + NSArray *a = [NSArray arrayWithObjects:@"CFBundleIdentifier", nil]; + NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; + CFDictionaryRef options = (CFDictionaryRef)optionsDict; + CFDictionaryRef result = nil; + check_error(AMDeviceLookupApplications(device, options, &result)); + + bool appExists = CFDictionaryContainsKey(result, cf_bundle_id); + NSLogOut(@"%@", appExists ? @"true" : @"false"); + CFRelease(cf_bundle_id); + + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + if (appExists) + return 0; + return -1; +} + +void list_bundle_id(AMDeviceRef device) +{ + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + NSArray *a = [NSArray arrayWithObjects:@"CFBundleIdentifier", nil]; + NSDictionary *optionsDict = [NSDictionary dictionaryWithObject:a forKey:@"ReturnAttributes"]; + CFDictionaryRef options = (CFDictionaryRef)optionsDict; + CFDictionaryRef result = nil; + check_error(AMDeviceLookupApplications(device, options, &result)); + + CFIndex count; + count = CFDictionaryGetCount(result); + const void *keys[count]; + CFDictionaryGetKeysAndValues(result, keys, NULL); + for(int i = 0; i < count; ++i) { + NSLogOut(@"%@", (CFStringRef)keys[i]); + } + + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); +} + +void copy_file_callback(afc_connection* afc_conn_p, const char *name,int file) +{ + const char *local_name=name; + + if (*local_name=='/') local_name++; + + if (*local_name=='\0') return; + + if (file) { + afc_file_ref fref; + int err = AFCFileRefOpen(afc_conn_p,name,1,&fref); + + if (err) { + fprintf(stderr,"AFCFileRefOpen(\"%s\") failed: %d\n",name,err); + return; + } + + FILE *fp = fopen(local_name,"w"); + + if (fp==NULL) { + fprintf(stderr,"fopen(\"%s\",\"w\") failer: %s\n",local_name,strerror(errno)); + AFCFileRefClose(afc_conn_p,fref); + return; + } + + char buf[4096]; + size_t sz=sizeof(buf); + + while (AFCFileRefRead(afc_conn_p,fref,buf,&sz)==0 && sz) { + fwrite(buf,sz,1,fp); + sz = sizeof(buf); + } + + AFCFileRefClose(afc_conn_p,fref); + fclose(fp); + } else { + if (mkdir(local_name,0777) && errno!=EEXIST) + fprintf(stderr,"mkdir(\"%s\") failed: %s\n",local_name,strerror(errno)); + } +} + +void download_tree(AMDeviceRef device) +{ + service_conn_t houseFd = start_house_arrest_service(device); + afc_connection* afc_conn_p = NULL; + char *dirname = NULL; + + list_root = list_root? list_root : "/"; + target_filename = target_filename? target_filename : "."; + + NSString* targetPath = [NSString pathWithComponents:@[ @(target_filename), @(list_root)] ]; + mkdirp([targetPath stringByDeletingLastPathComponent]); + + if (AFCConnectionOpen(houseFd, 0, &afc_conn_p) == 0) do { + + if (target_filename) { + dirname = strdup(target_filename); + mkdirp(@(dirname)); + if (mkdir(dirname,0777) && errno!=EEXIST) { + fprintf(stderr,"mkdir(\"%s\") failed: %s\n",dirname,strerror(errno)); + break; + } + if (chdir(dirname)) { + fprintf(stderr,"chdir(\"%s\") failed: %s\n",dirname,strerror(errno)); + break; + } + } + + read_dir(houseFd, afc_conn_p, list_root, copy_file_callback); + + } while(0); + + if (dirname) free(dirname); + if (afc_conn_p) AFCConnectionClose(afc_conn_p); +} + +void upload_dir(AMDeviceRef device, afc_connection* afc_conn_p, NSString* source, NSString* destination); +void upload_single_file(AMDeviceRef device, afc_connection* afc_conn_p, NSString* sourcePath, NSString* destinationPath); + +void upload_file(AMDeviceRef device) +{ + service_conn_t houseFd = start_house_arrest_service(device); + + afc_connection afc_conn; + afc_connection* afc_conn_p = &afc_conn; + AFCConnectionOpen(houseFd, 0, &afc_conn_p); + + // read_dir(houseFd, NULL, "/", NULL); + + if (!target_filename) + { + target_filename = get_filename_from_path(upload_pathname); + } + + NSString* sourcePath = [NSString stringWithUTF8String: upload_pathname]; + NSString* destinationPath = [NSString stringWithUTF8String: target_filename]; + + BOOL isDir; + bool exists = [[NSFileManager defaultManager] fileExistsAtPath: sourcePath isDirectory: &isDir]; + if (!exists) + { + on_error(@"Could not find file: %s", upload_pathname); + } + else if (isDir) + { + upload_dir(device, afc_conn_p, sourcePath, destinationPath); + } + else + { + upload_single_file(device, afc_conn_p, sourcePath, destinationPath); + } + assert(AFCConnectionClose(afc_conn_p) == 0); +} + +void upload_single_file(AMDeviceRef device, afc_connection* afc_conn_p, NSString* sourcePath, NSString* destinationPath) { + + afc_file_ref file_ref; + + // read_dir(houseFd, NULL, "/", NULL); + + size_t file_size; + void* file_content = read_file_to_memory([sourcePath fileSystemRepresentation], &file_size); + + if (!file_content) + { + on_error(@"Could not open file: %@", sourcePath); + } + + // Make sure the directory was created + { + NSString *dirpath = [destinationPath stringByDeletingLastPathComponent]; + check_error(AFCDirectoryCreate(afc_conn_p, [dirpath fileSystemRepresentation])); + } + + + int ret = AFCFileRefOpen(afc_conn_p, [destinationPath fileSystemRepresentation], 3, &file_ref); + if (ret == 0x000a) { + on_error(@"Cannot write to %@. Permission error.", destinationPath); + } + if (ret == 0x0009) { + on_error(@"Target %@ is a directory.", destinationPath); + } + assert(ret == 0); + assert(AFCFileRefWrite(afc_conn_p, file_ref, file_content, file_size) == 0); + assert(AFCFileRefClose(afc_conn_p, file_ref) == 0); + + free(file_content); +} + +void upload_dir(AMDeviceRef device, afc_connection* afc_conn_p, NSString* source, NSString* destination) +{ + check_error(AFCDirectoryCreate(afc_conn_p, [destination fileSystemRepresentation])); + destination = [destination copy]; + for (NSString* item in [[NSFileManager defaultManager] contentsOfDirectoryAtPath: source error: nil]) + { + NSString* sourcePath = [source stringByAppendingPathComponent: item]; + NSString* destinationPath = [destination stringByAppendingPathComponent: item]; + BOOL isDir; + [[NSFileManager defaultManager] fileExistsAtPath: sourcePath isDirectory: &isDir]; + if (isDir) + { + upload_dir(device, afc_conn_p, sourcePath, destinationPath); + } + else + { + upload_single_file(device, afc_conn_p, sourcePath, destinationPath); + } + } +} + +void make_directory(AMDeviceRef device) { + service_conn_t houseFd = start_house_arrest_service(device); + + afc_connection afc_conn; + afc_connection* afc_conn_p = &afc_conn; + AFCConnectionOpen(houseFd, 0, &afc_conn_p); + + assert(AFCDirectoryCreate(afc_conn_p, target_filename) == 0); + assert(AFCConnectionClose(afc_conn_p) == 0); +} + +void remove_path(AMDeviceRef device) { + service_conn_t houseFd = start_house_arrest_service(device); + + afc_connection afc_conn; + afc_connection* afc_conn_p = &afc_conn; + AFCConnectionOpen(houseFd, 0, &afc_conn_p); + + + assert(AFCRemovePath(afc_conn_p, target_filename) == 0); + assert(AFCConnectionClose(afc_conn_p) == 0); +} + +void iter_dir(struct afc_connection *conn, char const *path, iter_callback callback) { + struct afc_directory *dir; + char *dirent; + + if(AFCDirectoryOpen(conn, path, &dir)) + return; + + while(1) { + assert(AFCDirectoryRead(conn, dir, &dirent) == 0); + + if (!dirent) + break; + + if (strcmp(dirent, ".") == 0 || strcmp(dirent, "..") == 0) + continue; + + callback(conn, path, dirent); + } +} + +void remove_path_callback(struct afc_connection *conn, char const *path, char *dirent) { + char subdir[255]; + snprintf(subdir, 255, "%s/%s", path, dirent); + remove_path_recursively_conn(conn, subdir); +} + +void remove_path_recursively_conn(struct afc_connection *conn, char const *path) { + int ret = AFCRemovePath(conn, path); + if(ret == 0 || ret == 8) { + // Successfully removed (it was empty) or does not exist + NSLogVerbose(@"Deleted %s", path); + return; + } + + iter_dir(conn, path, (iter_callback) remove_path_callback); + + ret = AFCRemovePath(conn, path); + + if (ret == 10) + { + // No permissions for deleting folder + NSLogVerbose(@"No permissions for delete %s", path); + return; + } + + NSLogVerbose(@"Deleted %s", path); +} + +void remove_path_recursively(AMDeviceRef device) { + service_conn_t houseFd = start_house_arrest_service(device); + + afc_connection afc_conn; + afc_connection* afc_conn_p = &afc_conn; + AFCConnectionOpen(houseFd, 0, &afc_conn_p); + + remove_path_recursively_conn(afc_conn_p, target_filename); + + assert(AFCConnectionClose(afc_conn_p) == 0); +} + +void uninstall_app(AMDeviceRef device) { + CFRetain(device); // don't know if this is necessary? + + NSLogOut(@"------ Uninstall phase ------"); + + //Do we already have the bundle_id passed in via the command line? if so, use it. + CFStringRef cf_uninstall_bundle_id = NULL; + if (bundle_id != NULL) + { + cf_uninstall_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); + } else { + on_error(@"Error: you need to pass in the bundle id, (i.e. --bundle_id com.my.app)"); + } + + if (cf_uninstall_bundle_id == NULL) { + on_error(@"Error: Unable to get bundle id from user command or package %@.\nUninstall failed.", [NSString stringWithUTF8String:app_path]); + } else { + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + int code = AMDeviceSecureUninstallApplication(0, device, cf_uninstall_bundle_id, 0, NULL, 0); + if (code == 0) { + NSLogOut(@"[ OK ] Uninstalled package with bundle id %@", cf_uninstall_bundle_id); + } else { + on_error(@"[ ERROR ] Could not uninstall package with bundle id %@", cf_uninstall_bundle_id); + } + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + } +} + +void handle_device(AMDeviceRef device) { + NSLogVerbose(@"Already found device? %d", found_device); + + CFStringRef found_device_id = AMDeviceCopyDeviceIdentifier(device), + device_full_name = get_device_full_name(device), + device_interface_name = get_device_interface_name(device); + + if (detect_only) { + NSLogOut(@"[....] Found %@ connected through %@.", device_full_name, device_interface_name); + found_device = true; + return; + } + if (device_id != NULL) { + CFStringRef deviceCFSTR = CFStringCreateWithCString(NULL, device_id, kCFStringEncodingUTF8); + if (CFStringCompare(deviceCFSTR, found_device_id, kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + found_device = true; + CFRelease(deviceCFSTR); + } else { + NSLogOut(@"Skipping %@.", device_full_name); + return; + } + } else { + device_id = MYCFStringCopyUTF8String(found_device_id); + found_device = true; + } + + NSLogOut(@"[....] Using %@.", device_full_name); + + if (command_only) { + if (strcmp("list", command) == 0) { + list_files(device); + } else if (strcmp("upload", command) == 0) { + upload_file(device); + } else if (strcmp("download", command) == 0) { + download_tree(device); + } else if (strcmp("mkdir", command) == 0) { + make_directory(device); + } else if (strcmp("rm", command) == 0) { + remove_path(device); + } else if (strcmp("rm_r", command) == 0) { + remove_path_recursively(device); + } else if (strcmp("exists", command) == 0) { + exit(app_exists(device)); + } else if (strcmp("uninstall_only", command) == 0) { + uninstall_app(device); + } else if (strcmp("list_bundle_id", command) == 0) { + list_bundle_id(device); + } + exit(0); + } + + + CFRetain(device); // don't know if this is necessary? + + CFStringRef path = CFStringCreateWithCString(NULL, app_path, kCFStringEncodingUTF8); + CFURLRef relative_url = CFURLCreateWithFileSystemPath(NULL, path, kCFURLPOSIXPathStyle, false); + CFURLRef url = CFURLCopyAbsoluteURL(relative_url); + + CFRelease(relative_url); + + if (uninstall) { + NSLogOut(@"------ Uninstall phase ------"); + + //Do we already have the bundle_id passed in via the command line? if so, use it. + CFStringRef cf_uninstall_bundle_id = NULL; + if (bundle_id != NULL) + { + cf_uninstall_bundle_id = CFStringCreateWithCString(NULL, bundle_id, kCFStringEncodingUTF8); + } else { + cf_uninstall_bundle_id = get_bundle_id(url); + } + + if (cf_uninstall_bundle_id == NULL) { + on_error(@"Error: Unable to get bundle id from user command or package %@.\nUninstall failed.", [NSString stringWithUTF8String:app_path]); + } else { + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + int code = AMDeviceSecureUninstallApplication(0, device, cf_uninstall_bundle_id, 0, NULL, 0); + if (code == 0) { + NSLogOut(@"[ OK ] Uninstalled package with bundle id %@", cf_uninstall_bundle_id); + } else { + on_error(@"[ ERROR ] Could not uninstall package with bundle id %@", cf_uninstall_bundle_id); + } + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + } + } + + if(install) { + NSLogOut(@"------ Install phase ------"); + NSLogOut(@"[ 0%%] Found %@ connected through %@, beginning install", device_full_name, device_interface_name); + + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + + // NOTE: the secure version doesn't seem to require us to start the AFC service + service_conn_t afcFd; + check_error(AMDeviceSecureStartService(device, CFSTR("com.apple.afc"), NULL, &afcFd)); + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + + CFStringRef keys[] = { CFSTR("PackageType") }; + CFStringRef values[] = { CFSTR("Developer") }; + CFDictionaryRef options = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + //assert(AMDeviceTransferApplication(afcFd, path, NULL, transfer_callback, NULL) == 0); + check_error(AMDeviceSecureTransferPath(0, device, url, options, transfer_callback, 0)); + + close(afcFd); + + + + AMDeviceConnect(device); + assert(AMDeviceIsPaired(device)); + check_error(AMDeviceValidatePairing(device)); + check_error(AMDeviceStartSession(device)); + + // // NOTE: the secure version doesn't seem to require us to start the installation_proxy service + // // Although I can't find it right now, I in some code that the first param of AMDeviceSecureInstallApplication was a "dontStartInstallProxy" + // // implying this is done for us by iOS already + + //service_conn_t installFd; + //assert(AMDeviceSecureStartService(device, CFSTR("com.apple.mobile.installation_proxy"), NULL, &installFd) == 0); + + //mach_error_t result = AMDeviceInstallApplication(installFd, path, options, install_callback, NULL); + check_error(AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0)); + + // close(installFd); + + check_error(AMDeviceStopSession(device)); + check_error(AMDeviceDisconnect(device)); + + CFRelease(path); + CFRelease(options); + + NSLogOut(@"[100%%] Installed package %@", [NSString stringWithUTF8String:app_path]); + } + + if (!debug) + exit(0); // no debug phase + + if (justlaunch) { + launch_debugger_and_exit(device, url); + } else if (debugserver_only) { + launch_debugserver_only(device, url); + } else { + launch_debugger(device, url); + } +} + +void device_callback(struct am_device_notification_callback_info *info, void *arg) { + switch (info->msg) { + case ADNCI_MSG_CONNECTED: + if(device_id != NULL || !debug || AMDeviceGetInterfaceType(info->dev) != 2) { + if (no_wifi && AMDeviceGetInterfaceType(info->dev) == 2) + { + NSLogVerbose(@"Skipping wifi device (type: %d)", AMDeviceGetInterfaceType(info->dev)); + } + else + { + NSLogVerbose(@"Handling device type: %d", AMDeviceGetInterfaceType(info->dev)); + handle_device(info->dev); + } + } else if(best_device_match == NULL) { + NSLogVerbose(@"Best device match: %d", AMDeviceGetInterfaceType(info->dev)); + best_device_match = info->dev; + CFRetain(best_device_match); + } + default: + break; + } +} + +void timeout_callback(CFRunLoopTimerRef timer, void *info) { + if (found_device && (!detect_only)) { + // Don't need to exit in the justlaunch mode + if (justlaunch) + return; + + // App running for too long + NSLog(@"[ !! ] App is running for too long"); + exit(exitcode_timeout); + return; + } else if ((!found_device) && (!detect_only)) { + // Device not found timeout + if (best_device_match != NULL) { + NSLogVerbose(@"Handling best device match."); + handle_device(best_device_match); + + CFRelease(best_device_match); + best_device_match = NULL; + } + + if (!found_device) + on_error(@"Timed out waiting for device."); + } + else + { + // Device detection timeout + if (!debug) { + NSLogOut(@"[....] No more devices found."); + } + + if (detect_only && !found_device) { + exit(exitcode_error); + return; + } else { + int mypid = getpid(); + if ((parent != 0) && (parent == mypid) && (child != 0)) + { + NSLogVerbose(@"Timeout. Killing child (%d) tree.", child); + kill_ptree(child, SIGHUP); + } + } + exit(0); + } +} + +void usage(const char* app) { + NSLog( + @"Usage: %@ [OPTION]...\n" + @" -d, --debug launch the app in lldb after installation\n" + @" -i, --id the id of the device to connect to\n" + @" -c, --detect only detect if the device is connected\n" + @" -b, --bundle the path to the app bundle to be installed\n" + @" -s, --symbols the path to symbols\n" + @" -a, --args command line arguments to pass to the app when launching it\n" + @" -t, --timeout number of seconds to wait for a device to be connected\n" + @" -u, --unbuffered don't buffer stdout\n" + @" -n, --nostart do not start the app when debugging\n" + @" -N, --nolldb start debugserver only. do not run lldb\n" + @" -I, --noninteractive start in non interactive mode (quit when app crashes or exits)\n" + @" -L, --justlaunch just launch the app and exit lldb\n" + @" -v, --verbose enable verbose output\n" + @" -m, --noinstall directly start debugging without app install (-d not required)\n" + @" -p, --port port used for device, default: dynamic\n" + @" -r, --uninstall uninstall the app before install (do not use with -m; app cache and data are cleared) \n" + @" -9, --uninstall_only uninstall the app ONLY. Use only with -1 \n" + @" -1, --bundle_id specify bundle id for list and upload\n" + @" -l, --list list files\n" + @" -o, --upload upload file\n" + @" -w, --download download app tree\n" + @" -2, --to use together with up/download file/tree. specify target\n" + @" -D, --mkdir make directory on device\n" + @" -R, --rm remove file or directory on device (directories must be empty)\n" + @" -T, --rm_r remove file or directory recursively on device\n" + @" -V, --version print the executable version \n" + @" -e, --exists check if the app with given bundle_id is installed or not \n" + @" -B, --list_bundle_id list bundle_id \n" + @" -W, --no-wifi ignore wifi devices\n" + @" --detect_deadlocks start printing backtraces for all threads periodically after specific amount of seconds\n", + [NSString stringWithUTF8String:app]); +} + +void show_version() { + NSLogOut(@"%@", @ +#include "version.h" + ); +} + +int main(int argc, char *argv[]) { + + // create a UUID for tmp purposes + CFUUIDRef uuid = CFUUIDCreate(NULL); + CFStringRef str = CFUUIDCreateString(NULL, uuid); + CFRelease(uuid); + tmpUUID = [(NSString*)str autorelease]; + + static struct option longopts[] = { + { "debug", no_argument, NULL, 'd' }, + { "id", required_argument, NULL, 'i' }, + { "bundle", required_argument, NULL, 'b' }, + { "symbols", required_argument, NULL, 's' }, + { "args", required_argument, NULL, 'a' }, + { "verbose", no_argument, NULL, 'v' }, + { "timeout", required_argument, NULL, 't' }, + { "unbuffered", no_argument, NULL, 'u' }, + { "nostart", no_argument, NULL, 'n' }, + { "nolldb", no_argument, NULL, 'N' }, + { "noninteractive", no_argument, NULL, 'I' }, + { "justlaunch", no_argument, NULL, 'L' }, + { "detect", no_argument, NULL, 'c' }, + { "version", no_argument, NULL, 'V' }, + { "noinstall", no_argument, NULL, 'm' }, + { "port", required_argument, NULL, 'p' }, + { "uninstall", no_argument, NULL, 'r' }, + { "uninstall_only", no_argument, NULL, '9'}, + { "list", optional_argument, NULL, 'l' }, + { "bundle_id", required_argument, NULL, '1'}, + { "upload", required_argument, NULL, 'o'}, + { "download", optional_argument, NULL, 'w'}, + { "to", required_argument, NULL, '2'}, + { "mkdir", required_argument, NULL, 'D'}, + { "rm", required_argument, NULL, 'R'}, + { "rm_r", required_argument, NULL, 'T'}, + { "exists", no_argument, NULL, 'e'}, + { "list_bundle_id", no_argument, NULL, 'B'}, + { "no-wifi", no_argument, NULL, 'W'}, + { "detect_deadlocks", required_argument, NULL, 1000 }, + { NULL, 0, NULL, 0 }, + }; + int ch; + + while ((ch = getopt_long(argc, argv, "VmcdvunNrILeD:R:i:b:s:a:t:g:x:p:1:2:o:l::w::9::B::W", longopts, NULL)) != -1) + { + switch (ch) { + case 'm': + install = false; + debug = true; + break; + case 'd': + debug = true; + break; + case 'i': + device_id = optarg; + break; + case 'b': + app_path = optarg; + break; + case 's': + disk_symbol_path = optarg; + break; + case 'a': + args = optarg; + break; + case 'v': + verbose = true; + break; + case 't': + _timeout = atoi(optarg); + break; + case 'u': + unbuffered = true; + break; + case 'n': + nostart = true; + break; + case 'N': + debugserver_only = true; + debug = true; + break; + case 'I': + interactive = false; + debug = true; + break; + case 'L': + interactive = false; + justlaunch = true; + debug = true; + break; + case 'c': + detect_only = true; + debug = true; + break; + case 'V': + show_version(); + return 0; + case 'p': + port = atoi(optarg); + break; + case 'r': + uninstall = true; + break; + case '9': + command_only = true; + command = "uninstall_only"; + break; + case '1': + bundle_id = optarg; + break; + case '2': + target_filename = optarg; + break; + case 'o': + command_only = true; + upload_pathname = optarg; + command = "upload"; + break; + case 'l': + command_only = true; + command = "list"; + list_root = optarg; + break; + case 'w': + command_only = true; + command = "download"; + list_root = optarg; + break; + case 'D': + command_only = true; + target_filename = optarg; + command = "mkdir"; + break; + case 'R': + command_only = true; + target_filename = optarg; + command = "rm"; + break; + case 'T': + command_only = true; + target_filename = optarg; + command = "rm_r"; + break; + case 'e': + command_only = true; + command = "exists"; + break; + case 'B': + command_only = true; + command = "list_bundle_id"; + break; + case 'W': + no_wifi = true; + break; + case 1000: + _detectDeadlockTimeout = atoi(optarg); + break; + default: + usage(argv[0]); + return exitcode_error; + } + } + + if (!app_path && !detect_only && !command_only) { + usage(argv[0]); + on_error(@"One of -[b|c|o|l|w|D|R|e|9] is required to proceed!"); + } + + if (unbuffered) { + setbuf(stdout, NULL); + setbuf(stderr, NULL); + } + + if (detect_only && _timeout == 0) { + _timeout = 5; + } + + if (app_path) { + if (access(app_path, F_OK) != 0) { + on_sys_error(@"Can't access app path '%@'", [NSString stringWithUTF8String:app_path]); + } + } + + AMDSetLogLevel(5); // otherwise syslog gets flooded with crap + if (_timeout > 0) + { + CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent() + _timeout, 0, 0, 0, timeout_callback, NULL); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes); + NSLogOut(@"[....] Waiting up to %d seconds for iOS device to be connected", _timeout); + } + else + { + NSLogOut(@"[....] Waiting for iOS device to be connected"); + } + + AMDeviceNotificationSubscribe(&device_callback, 0, 0, NULL, ¬ify); + CFRunLoopRun(); +} diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/version.h b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/version.h new file mode 100644 index 000000000000..65be40a270ef --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/ios-deploy/version.h @@ -0,0 +1 @@ +"1.9.4" diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/check_reqs.js b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/check_reqs.js new file mode 100644 index 000000000000..38c886e67c73 --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/check_reqs.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +var util = require('util'); +var os = require('os'); +var child_process = require('child_process'); + +var XCODEBUILD_MIN_VERSION = 7.0; +var XCODEBUILD_NOT_FOUND_MESSAGE = util.format('Please install Xcode version %s or greater from the Mac App Store.', XCODEBUILD_MIN_VERSION); +var TOOL = 'xcodebuild'; + +var xcode_version = child_process.spawn(TOOL, ['-version']), + version_string = ''; + +xcode_version.stdout.on('data', function (data) { + version_string += data; +}); + +xcode_version.stderr.on('data', function (data) { + console.log('stderr: ' + data); +}); + +xcode_version.on('error', function (err) { + console.log(util.format('Tool %s was not found. %s', TOOL, XCODEBUILD_NOT_FOUND_MESSAGE)); +}); + +xcode_version.on('close', function (code) { + if (code === 0) { + var arr = version_string.match(/^Xcode (\d+\.\d+)/); + var ver = arr[1]; + + if (os.release() >= '15.0.0' && ver < XCODEBUILD_MIN_VERSION) { + console.log(util.format('You need at least Xcode 7.0 when you are on OS X 10.11 El Capitan (you have version %s)', ver)); + process.exit(1); + } + + if (ver < XCODEBUILD_MIN_VERSION) { + console.log(util.format('%s : %s. (you have version %s)', TOOL, XCODEBUILD_NOT_FOUND_MESSAGE, ver)); + } + + if (os.release() >= '15.0.0') { // print the El Capitan warning + console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + console.log('!!!! WARNING: You are on OS X 10.11 El Capitan or greater, you may need to add the'); + console.log('!!!! WARNING: `--unsafe-perm=true` flag when running `npm install`'); + console.log('!!!! WARNING: or else it will fail.'); + console.log('!!!! WARNING: link:'); + console.log('!!!! WARNING: https://github.com/phonegap/ios-deploy#os-x-1011-el-capitan-or-greater'); + console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + } + + } + process.exit(code); +}); + + + + diff --git a/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/lldb.py b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/lldb.py new file mode 100644 index 000000000000..d4157d43feeb --- /dev/null +++ b/Engine/Extras/ThirdPartyNotUE/ios-deploy/src/scripts/lldb.py @@ -0,0 +1,144 @@ +import time +import os +import sys +import shlex +import lldb + +listener = None + +def connect_command(debugger, command, result, internal_dict): + # These two are passed in by the script which loads us + connect_url = internal_dict['fruitstrap_connect_url'] + error = lldb.SBError() + + # We create a new listener here and will use it for both target and the process. + # It allows us to prevent data races when both our code and internal lldb code + # try to process STDOUT/STDERR messages + global listener + listener = lldb.SBListener('iosdeploy_listener') + + listener.StartListeningForEventClass(debugger, + lldb.SBTarget.GetBroadcasterClassName(), + lldb.SBProcess.eBroadcastBitStateChanged | lldb.SBProcess.eBroadcastBitSTDOUT | lldb.SBProcess.eBroadcastBitSTDERR) + + process = lldb.target.ConnectRemote(listener, connect_url, None, error) + + # Wait for connection to succeed + events = [] + state = (process.GetState() or lldb.eStateInvalid) + while state != lldb.eStateConnected: + event = lldb.SBEvent() + if listener.WaitForEvent(1, event): + state = process.GetStateFromEvent(event) + events.append(event) + else: + state = lldb.eStateInvalid + + # Add events back to queue, otherwise lldb freezes + for event in events: + listener.AddEvent(event) + +def run_command(debugger, command, result, internal_dict): + device_app = internal_dict['fruitstrap_device_app'] + args = command.split('--',1) + error = lldb.SBError() + lldb.target.modules[0].SetPlatformFileSpec(lldb.SBFileSpec(device_app)) + args_arr = [] + if len(args) > 1: + args_arr = shlex.split(args[1]) + + # EPIC: Specify not to use posix to maintain quotes, newlines, etc in passed arguments + args_arr = args_arr + shlex.split('{args}', posix=False) + + launchInfo = lldb.SBLaunchInfo(args_arr) + global listener + launchInfo.SetListener(listener) + + #This env variable makes NSLog, CFLog and os_log messages get mirrored to stderr + #https://stackoverflow.com/a/39581193 + launchInfo.SetEnvironmentEntries(['OS_ACTIVITY_DT_MODE=enable'], True) + + lldb.target.Launch(launchInfo, error) + lockedstr = ': Locked' + if lockedstr in str(error): + print('\\nDevice Locked\\n') + os._exit(254) + else: + print(str(error)) + +def safequit_command(debugger, command, result, internal_dict): + process = lldb.target.process + state = process.GetState() + if state == lldb.eStateRunning: + process.Detach() + os._exit(0) + elif state > lldb.eStateRunning: + os._exit(state) + else: + print('\\nApplication has not been launched\\n') + os._exit(1) + +def autoexit_command(debugger, command, result, internal_dict): + global listener + process = lldb.target.process + + detectDeadlockTimeout = {detect_deadlock_timeout} + printBacktraceTime = time.time() + detectDeadlockTimeout if detectDeadlockTimeout > 0 else None + + # This line prevents internal lldb listener from processing STDOUT/STDERR messages. Without it, an order of log writes is incorrect sometimes + debugger.GetListener().StopListeningForEvents(process.GetBroadcaster(), lldb.SBProcess.eBroadcastBitSTDOUT | lldb.SBProcess.eBroadcastBitSTDERR ) + + event = lldb.SBEvent() + + def ProcessSTDOUT(): + stdout = process.GetSTDOUT(1024) + while stdout: + sys.stdout.write(stdout) + stdout = process.GetSTDOUT(1024) + + def ProcessSTDERR(): + stderr = process.GetSTDERR(1024) + while stderr: + sys.stdout.write(stderr) + stderr = process.GetSTDERR(1024) + + while True: + if listener.WaitForEvent(1, event) and lldb.SBProcess.EventIsProcessEvent(event): + state = lldb.SBProcess.GetStateFromEvent(event) + type = event.GetType() + + if type & lldb.SBProcess.eBroadcastBitSTDOUT: + ProcessSTDOUT() + + if type & lldb.SBProcess.eBroadcastBitSTDERR: + ProcessSTDERR() + + else: + state = process.GetState() + + if state != lldb.eStateRunning: + # Let's make sure that we drained our streams before exit + ProcessSTDOUT() + ProcessSTDERR() + + if state == lldb.eStateExited: + sys.stdout.write( '\\nPROCESS_EXITED\\n' ) + os._exit(process.GetExitStatus()) + elif printBacktraceTime is None and state == lldb.eStateStopped: + sys.stdout.write( '\\nPROCESS_STOPPED\\n' ) + debugger.HandleCommand('thread backtrace all -c 50') + os._exit({exitcode_app_crash}) + elif state == lldb.eStateCrashed: + sys.stdout.write( '\\nPROCESS_CRASHED\\n' ) + debugger.HandleCommand('thread backtrace all -c 50') + os._exit({exitcode_app_crash}) + elif state == lldb.eStateDetached: + sys.stdout.write( '\\nPROCESS_DETACHED\\n' ) + os._exit({exitcode_app_crash}) + elif printBacktraceTime is not None and time.time() >= printBacktraceTime: + printBacktraceTime = None + sys.stdout.write( '\\nPRINT_BACKTRACE_TIMEOUT\\n' ) + debugger.HandleCommand('process interrupt') + debugger.HandleCommand('bt all') + debugger.HandleCommand('continue') + printBacktraceTime = time.time() + 5 diff --git a/Engine/Plugins/2D/Paper2D/Source/Paper2DEditor/Private/Atlasing/PaperAtlasGenerator.cpp b/Engine/Plugins/2D/Paper2D/Source/Paper2DEditor/Private/Atlasing/PaperAtlasGenerator.cpp index 276013251374..c0c2e21ed709 100644 --- a/Engine/Plugins/2D/Paper2D/Source/Paper2DEditor/Private/Atlasing/PaperAtlasGenerator.cpp +++ b/Engine/Plugins/2D/Paper2D/Source/Paper2DEditor/Private/Atlasing/PaperAtlasGenerator.cpp @@ -69,11 +69,16 @@ void FPaperAtlasGenerator::HandleAssetChangedEvent(UPaperSpriteAtlas* Atlas) bWasTextureRemoved = true; } } + if (bWasTextureRemoved) { MergeAdjacentRects(Atlas); } + // Sort sprites by name first, so that we have a consistent sorting, before sorting by size, so that we have a repeatable + // atlas generation. + SpritesInNewAtlas.StableSort([](UPaperSprite& A, UPaperSprite& B) { return A.GetPathName() > B.GetPathName(); } ); + // Sort new sprites by size struct Local { @@ -84,7 +89,7 @@ void FPaperAtlasGenerator::HandleAssetChangedEvent(UPaperSpriteAtlas* Atlas) return SpriteSize.X * 16384 + SpriteSize.Y; // Sort wider textures first } }; - SpritesInNewAtlas.Sort( [](UPaperSprite& A, UPaperSprite& B) { return Local::SpriteSortValue(A) > Local::SpriteSortValue(B); } ); + SpritesInNewAtlas.StableSort( [](UPaperSprite& A, UPaperSprite& B) { return Local::SpriteSortValue(A) > Local::SpriteSortValue(B); } ); // Add new sprites TArray ImprovementTestAtlas; // A second atlas to compare wastage diff --git a/Engine/Plugins/Compositing/Composure/Source/Composure/Classes/ComposureBlueprintLibrary.h b/Engine/Plugins/Compositing/Composure/Source/Composure/Classes/ComposureBlueprintLibrary.h index 3c34ff98e2a4..7cad969ff847 100644 --- a/Engine/Plugins/Compositing/Composure/Source/Composure/Classes/ComposureBlueprintLibrary.h +++ b/Engine/Plugins/Compositing/Composure/Source/Composure/Classes/ComposureBlueprintLibrary.h @@ -58,7 +58,7 @@ class UComposureBlueprintLibrary : public UBlueprintFunctionLibrary /** Returns the red and green channel factors from percentage of chromatic aberration. */ UFUNCTION(BlueprintPure, Category = "Composure") - static void GetRedGreenUVFactorsFromChromaticAberration(float ChromaticAberrationAmount, FVector2D& RedGreenUVFactors); + static void GetRedGreenUVFactorsFromChromaticAberration(float ChromaticAberrationAmount, FVector2D& RedGreenUVFactors); /** Returns display gamma of a given player camera manager, or 0 if no scene viewport attached. */ UFUNCTION(BlueprintPure, Category = "Composure") diff --git a/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceResponseTypes.h b/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceResponseTypes.h index f35f77395a6b..74421cef5876 100644 --- a/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceResponseTypes.h +++ b/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceResponseTypes.h @@ -15,25 +15,25 @@ struct FOneSkyListProjectGroupsResponseMeta GENERATED_USTRUCT_BODY() UPROPERTY() - int32 status; + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; UPROPERTY() - int32 page_count; + int32 page_count; UPROPERTY() - FString next_page; + FString next_page; UPROPERTY() - FString prev_page; + FString prev_page; UPROPERTY() - FString first_page; + FString first_page; UPROPERTY() - FString last_page; + FString last_page; /** Default constructor. */ FOneSkyListProjectGroupsResponseMeta() @@ -57,10 +57,10 @@ struct FOneSkyListProjectGroupsResponseDataItem GENERATED_USTRUCT_BODY() UPROPERTY() - FString id; + FString id; UPROPERTY() - FString name; + FString name; /** Default constructor. */ FOneSkyListProjectGroupsResponseDataItem() @@ -82,10 +82,10 @@ struct FOneSkyListProjectGroupsResponse GENERATED_USTRUCT_BODY() UPROPERTY() - FOneSkyListProjectGroupsResponseMeta meta; + FOneSkyListProjectGroupsResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListProjectGroupsResponse() @@ -107,8 +107,8 @@ struct FOneSkyShowProjectGroupResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; /** Default constructor. */ FOneSkyShowProjectGroupResponseMeta() @@ -125,16 +125,16 @@ struct FOneSkyShowProjectGroupResponseData GENERATED_USTRUCT_BODY() UPROPERTY() - FString id; + FString id; UPROPERTY() - FString name; + FString name; UPROPERTY() - int32 description; + int32 description; UPROPERTY() - FString project_count; + FString project_count; /** Default constructor. */ FOneSkyShowProjectGroupResponseData() @@ -157,11 +157,11 @@ struct FOneSkyShowProjectGroupResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyShowProjectGroupResponseMeta meta; + UPROPERTY() + FOneSkyShowProjectGroupResponseMeta meta; UPROPERTY() - FOneSkyShowProjectGroupResponseData data; + FOneSkyShowProjectGroupResponseData data; /** Default constructor. */ FOneSkyShowProjectGroupResponse() @@ -183,8 +183,8 @@ struct FOneSkyCreateProjectGroupResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; /** Default constructor. */ FOneSkyCreateProjectGroupResponseMeta() @@ -200,20 +200,20 @@ struct FOneSkyCreateProjectGroupResponseBaseLanguage { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString code; + UPROPERTY() + FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; /** Default constructor. */ FOneSkyCreateProjectGroupResponseBaseLanguage() @@ -233,14 +233,14 @@ struct FOneSkyCreateProjectGroupResponseData { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 id; + UPROPERTY() + int32 id; UPROPERTY() - FString name; + FString name; UPROPERTY() - FOneSkyCreateProjectGroupResponseBaseLanguage base_language; + FOneSkyCreateProjectGroupResponseBaseLanguage base_language; /** Default constructor. */ FOneSkyCreateProjectGroupResponseData() @@ -262,11 +262,11 @@ struct FOneSkyCreateProjectGroupResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyCreateProjectGroupResponseMeta meta; + UPROPERTY() + FOneSkyCreateProjectGroupResponseMeta meta; UPROPERTY() - FOneSkyCreateProjectGroupResponseData data; + FOneSkyCreateProjectGroupResponseData data; /** Default constructor. */ FOneSkyCreateProjectGroupResponse() @@ -288,11 +288,11 @@ struct FOneSkyListProjectGroupLanguagesResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; /** Default constructor. */ FOneSkyListProjectGroupLanguagesResponseMeta() @@ -309,23 +309,23 @@ struct FOneSkyListProjectGroupLanguagesResponseDataItem { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString code; + UPROPERTY() + FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; UPROPERTY() - bool is_base_language; + bool is_base_language; /** Default constructor. */ FOneSkyListProjectGroupLanguagesResponseDataItem() @@ -350,11 +350,11 @@ struct FOneSkyListProjectGroupLanguagesResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyListProjectGroupLanguagesResponseMeta meta; + UPROPERTY() + FOneSkyListProjectGroupLanguagesResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListProjectGroupLanguagesResponse() @@ -376,11 +376,11 @@ struct FOneSkyListProjectsInGroupResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; /** Default constructor. */ FOneSkyListProjectsInGroupResponseMeta() @@ -398,10 +398,10 @@ struct FOneSkyListProjectsInGroupResponseDataItem GENERATED_USTRUCT_BODY() UPROPERTY() - int32 id; + int32 id; UPROPERTY() - FString name; + FString name; /** Default constructor. */ FOneSkyListProjectsInGroupResponseDataItem() @@ -426,7 +426,7 @@ struct FOneSkyListProjectsInGroupResponse FOneSkyListProjectsInGroupResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListProjectsInGroupResponse() @@ -448,8 +448,8 @@ struct FOneSkyShowProjectResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; /** Default constructor. */ FOneSkyShowProjectResponseMeta() @@ -466,11 +466,11 @@ struct FOneSkyShowProjectResponseProjectType { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString code; + UPROPERTY() + FString code; UPROPERTY() - FString name; + FString name; /** Default constructor. */ FOneSkyShowProjectResponseProjectType() @@ -488,22 +488,22 @@ struct FOneSkyShowProjectResponseData GENERATED_USTRUCT_BODY() UPROPERTY() - int32 id; + int32 id; UPROPERTY() - FString name; + FString name; UPROPERTY() - FString description; + FString description; UPROPERTY() - FOneSkyShowProjectResponseProjectType project_type; + FOneSkyShowProjectResponseProjectType project_type; UPROPERTY() - int32 string_count; + int32 string_count; UPROPERTY() - int32 word_count; + int32 word_count; /** Default constructor. */ FOneSkyShowProjectResponseData() @@ -527,11 +527,11 @@ struct FOneSkyShowProjectResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyShowProjectResponseMeta meta; + UPROPERTY() + FOneSkyShowProjectResponseMeta meta; UPROPERTY() - FOneSkyShowProjectResponseData data; + FOneSkyShowProjectResponseData data; /** Default constructor. */ FOneSkyShowProjectResponse() @@ -554,7 +554,7 @@ struct FOneSkyCreateProjectResponseMeta GENERATED_USTRUCT_BODY() UPROPERTY() - int32 status; + int32 status; /** Default constructor. */ FOneSkyCreateProjectResponseMeta() @@ -572,10 +572,10 @@ struct FOneSkyCreateProjectResponseProjectType GENERATED_USTRUCT_BODY() UPROPERTY() - FString code; + FString code; UPROPERTY() - FString name; + FString name; /** Default constructor. */ FOneSkyCreateProjectResponseProjectType() @@ -593,16 +593,16 @@ struct FOneSkyCreateProjectResponseData GENERATED_USTRUCT_BODY() UPROPERTY() - int32 id; + int32 id; UPROPERTY() - FString name; + FString name; UPROPERTY() - FString description; + FString description; UPROPERTY() - FOneSkyShowProjectResponseProjectType project_type; + FOneSkyShowProjectResponseProjectType project_type; /** Default constructor. */ FOneSkyCreateProjectResponseData() @@ -651,10 +651,10 @@ struct FOneSkyListProjectLanguagesResponseMeta GENERATED_USTRUCT_BODY() UPROPERTY() - int32 status; + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; /** Default constructor. */ FOneSkyListProjectLanguagesResponseMeta() @@ -671,29 +671,29 @@ struct FOneSkyListProjectLanguagesResponseDataItem { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString code; + UPROPERTY() + FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; UPROPERTY() - bool is_base_language; + bool is_base_language; UPROPERTY() - bool is_ready_to_publish; + bool is_ready_to_publish; UPROPERTY() - FString translation_progress; + FString translation_progress; /** Default constructor. */ FOneSkyListProjectLanguagesResponseDataItem() @@ -721,10 +721,10 @@ struct FOneSkyListProjectLanguagesResponse GENERATED_USTRUCT_BODY() UPROPERTY() - FOneSkyListProjectLanguagesResponseMeta meta; + FOneSkyListProjectLanguagesResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListProjectLanguagesResponse() @@ -746,8 +746,8 @@ struct FOneSkyTranslationStatusResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; /** Default constructor. */ FOneSkyTranslationStatusResponseMeta() @@ -765,19 +765,19 @@ struct FOneSkyTranslationStatusResponseLocale GENERATED_USTRUCT_BODY() UPROPERTY() - FString code; + FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; /** Default constructor. */ FOneSkyTranslationStatusResponseLocale() @@ -798,19 +798,19 @@ struct FOneSkyTranslationStatusResponseData GENERATED_USTRUCT_BODY() UPROPERTY() - FString file_name; + FString file_name; UPROPERTY() - FOneSkyTranslationStatusResponseLocale locale; + FOneSkyTranslationStatusResponseLocale locale; UPROPERTY() - FString progress; + FString progress; UPROPERTY() - int32 string_count; + int32 string_count; UPROPERTY() - int32 word_count; + int32 word_count; /** Default constructor. */ FOneSkyTranslationStatusResponseData() @@ -835,10 +835,10 @@ struct FOneSkyTranslationStatusResponse GENERATED_USTRUCT_BODY() UPROPERTY() - FOneSkyTranslationStatusResponseMeta meta; + FOneSkyTranslationStatusResponseMeta meta; UPROPERTY() - FOneSkyTranslationStatusResponseData data; + FOneSkyTranslationStatusResponseData data; /** Default constructor. */ FOneSkyTranslationStatusResponse() @@ -860,26 +860,26 @@ struct FOneSkyListUploadedFilesResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; UPROPERTY() - int32 page_count; + int32 page_count; UPROPERTY() - FString next_page; + FString next_page; UPROPERTY() - FString prev_page; + FString prev_page; UPROPERTY() - FString first_page; + FString first_page; UPROPERTY() - FString last_page; + FString last_page; /** Default constructor. */ FOneSkyListUploadedFilesResponseMeta() @@ -902,10 +902,10 @@ struct FOneSkyListUploadedFilesResponseLastImport GENERATED_USTRUCT_BODY() UPROPERTY() - int32 id; + int32 id; UPROPERTY() - FString status; + FString status; /** Default constructor. */ FOneSkyListUploadedFilesResponseLastImport() @@ -924,19 +924,19 @@ struct FOneSkyListUploadedFilesResponseDataItem GENERATED_USTRUCT_BODY() UPROPERTY() - FString file_name; + FString file_name; UPROPERTY() - int32 string_count; + int32 string_count; UPROPERTY() - FOneSkyListUploadedFilesResponseLastImport last_import; + FOneSkyListUploadedFilesResponseLastImport last_import; UPROPERTY() - FString uploaded_at; + FString uploaded_at; UPROPERTY() - int32 uploaded_at_timestamp; + int32 uploaded_at_timestamp; /** Default constructor. */ FOneSkyListUploadedFilesResponseDataItem() @@ -960,11 +960,11 @@ struct FOneSkyListUploadedFilesResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyListUploadedFilesResponseMeta meta; + UPROPERTY() + FOneSkyListUploadedFilesResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListUploadedFilesResponse() @@ -986,7 +986,7 @@ struct FOneSkyUploadFileResponseMeta GENERATED_USTRUCT_BODY() UPROPERTY() - int32 status; + int32 status; /** Default constructor. */ FOneSkyUploadFileResponseMeta() @@ -1003,20 +1003,20 @@ struct FOneSkyUploadFileResponseLanguage { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString code; + UPROPERTY() + FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; /** Default constructor. */ FOneSkyUploadFileResponseLanguage() @@ -1037,13 +1037,13 @@ struct FOneSkyUploadedFileResponseImport GENERATED_USTRUCT_BODY() UPROPERTY() - int32 id; + int32 id; UPROPERTY() - FString created_at; + FString created_at; UPROPERTY() - int32 created_at_timestamp; + int32 created_at_timestamp; /** Default constructor. */ FOneSkyUploadedFileResponseImport() @@ -1061,17 +1061,17 @@ struct FOneSkyUploadFileResponseData { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString name; + UPROPERTY() + FString name; UPROPERTY() - FString format; + FString format; UPROPERTY() - FOneSkyUploadFileResponseLanguage language; + FOneSkyUploadFileResponseLanguage language; UPROPERTY() - FOneSkyUploadedFileResponseImport import; + FOneSkyUploadedFileResponseImport import; /** Default constructor. */ FOneSkyUploadFileResponseData() @@ -1098,7 +1098,7 @@ struct FOneSkyUploadFileResponse FOneSkyUploadFileResponseMeta meta; UPROPERTY() - FOneSkyUploadFileResponseData data; + FOneSkyUploadFileResponseData data; /** Default constructor. */ FOneSkyUploadFileResponse() @@ -1120,26 +1120,26 @@ struct FOneSkyListPhraseCollectionsResponseMeta { GENERATED_USTRUCT_BODY() - UPROPERTY() - int32 status; + UPROPERTY() + int32 status; UPROPERTY() - int32 record_count; + int32 record_count; UPROPERTY() - int32 page_count; + int32 page_count; UPROPERTY() - FString next_page; + FString next_page; UPROPERTY() - FString prev_page; + FString prev_page; UPROPERTY() - FString first_page; + FString first_page; UPROPERTY() - FString last_page; + FString last_page; /** Default constructor. */ FOneSkyListPhraseCollectionsResponseMeta() @@ -1161,14 +1161,14 @@ struct FOneSkyListPhraseCollectionsResponseDataItem { GENERATED_USTRUCT_BODY() - UPROPERTY() - FString key; + UPROPERTY() + FString key; UPROPERTY() - FString created_at; + FString created_at; UPROPERTY() - int32 created_at_timestamp; + int32 created_at_timestamp; /** Default constructor. */ FOneSkyListPhraseCollectionsResponseDataItem() @@ -1190,11 +1190,11 @@ struct FOneSkyListPhraseCollectionsResponse { GENERATED_USTRUCT_BODY() - UPROPERTY() - FOneSkyListPhraseCollectionsResponseMeta meta; + UPROPERTY() + FOneSkyListPhraseCollectionsResponseMeta meta; UPROPERTY() - TArray data; + TArray data; /** Default constructor. */ FOneSkyListPhraseCollectionsResponse() @@ -1237,16 +1237,16 @@ struct FOneSkyShowImportTaskResponseLocale FString code; UPROPERTY() - FString english_name; + FString english_name; UPROPERTY() - FString local_name; + FString local_name; UPROPERTY() - FString locale; + FString locale; UPROPERTY() - FString region; + FString region; /** Default constructor. */ FOneSkyShowImportTaskResponseLocale() diff --git a/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceSettings.h b/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceSettings.h index 57cc6e30cc05..894451e31d94 100644 --- a/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceSettings.h +++ b/Engine/Plugins/Developer/OneSkyLocalizationService/Source/OneSkyLocalizationService/Private/OneSkyLocalizationServiceSettings.h @@ -23,19 +23,19 @@ struct FOneSkyLocalizationTargetSetting * The GUID of the LocalizationTarget that these OneSky settings are for */ UPROPERTY() - FGuid TargetGuid; + FGuid TargetGuid; /** * The id of the OneSky Project this target belongs to */ UPROPERTY() - int32 OneSkyProjectId; + int32 OneSkyProjectId; /** * The name of the file that corresponds to this target */ UPROPERTY() - FString OneSkyFileName; + FString OneSkyFileName; }; diff --git a/Engine/Plugins/Developer/PerforceSourceControl/Source/PerforceSourceControl/Private/PerforceSourceControlOperations.cpp b/Engine/Plugins/Developer/PerforceSourceControl/Source/PerforceSourceControl/Private/PerforceSourceControlOperations.cpp index 8530ffab085d..0ceeb63187f6 100644 --- a/Engine/Plugins/Developer/PerforceSourceControl/Source/PerforceSourceControl/Private/PerforceSourceControlOperations.cpp +++ b/Engine/Plugins/Developer/PerforceSourceControl/Source/PerforceSourceControl/Private/PerforceSourceControlOperations.cpp @@ -754,7 +754,10 @@ static void ParseUpdateStatusResults(const FP4RecordSet& InRecords, const TArray const FString OtherOpenRecordKey = FString::Printf(TEXT("otherOpen%d"), OpenIdx); const FString OtherOpenRecordValue = ClientRecord(OtherOpenRecordKey); - BranchModification.OtherUserCheckedOut += OtherOpenRecordValue; + int32 AtIndex = OtherOpenRecordValue.Find(TEXT("@")); + FString OtherOpenUser = AtIndex == INDEX_NONE ? FString(TEXT("")) : OtherOpenRecordValue.Left(AtIndex); + BranchModification.OtherUserCheckedOut += OtherOpenUser + TEXT(" @ ") + BranchModification.BranchName; + if (OpenIdx < OtherOpenNum - 1) { BranchModification.OtherUserCheckedOut += TEXT(", "); @@ -792,6 +795,15 @@ static void ParseUpdateStatusResults(const FP4RecordSet& InRecords, const TArray FPerforceSourceControlState& State = OutStates.Last(); State.DepotFilename = DepotFileName; + FString Branch; + FString BranchFile; + if (DepotFileName.Split(ContentRoot, &Branch, &BranchFile)) + { + // Sanitize + Branch.RemoveFromEnd(FString(TEXT("/"))); + BranchFile.RemoveFromStart(FString(TEXT("/"))); + } + State.State = EPerforceState::ReadOnly; if (Action.Len() > 0 && Action == TEXT("add")) { @@ -821,7 +833,10 @@ static void ParseUpdateStatusResults(const FP4RecordSet& InRecords, const TArray const FString OtherOpenRecordKey = FString::Printf(TEXT("otherOpen%d"), OpenIdx); const FString OtherOpenRecordValue = ClientRecord(OtherOpenRecordKey); - State.OtherUserCheckedOut += OtherOpenRecordValue; + int32 AtIndex = OtherOpenRecordValue.Find(TEXT("@")); + FString OtherOpenUser = AtIndex == INDEX_NONE ? FString(TEXT("")) : OtherOpenRecordValue.Left(AtIndex); + State.OtherUserCheckedOut += OtherOpenUser + TEXT(" @ ") + Branch; + if(OpenIdx < OtherOpenNum - 1) { State.OtherUserCheckedOut += TEXT(", "); @@ -839,11 +854,6 @@ static void ParseUpdateStatusResults(const FP4RecordSet& InRecords, const TArray State.State = EPerforceState::NotInDepot; } - // If checked out or modified in another branch, setup state - FString BranchFile; - FileName.Replace(TEXT("\\"), TEXT("/")).Split(ContentRoot, nullptr, &BranchFile); - BranchFile.RemoveFromStart(TEXT("/")); - State.HeadBranch = TEXT("*CurrentBranch"); State.HeadAction = HeadAction; State.HeadModTime = FCString::Atoi64(*ClientRecord(TEXT("headModTime"))); diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/AssetManagerEditorModule.cpp b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/AssetManagerEditorModule.cpp index 84f1896c808d..fffd99af2296 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/AssetManagerEditorModule.cpp +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/AssetManagerEditorModule.cpp @@ -33,6 +33,7 @@ #include "Widgets/SToolTip.h" #include "PropertyCustomizationHelpers.h" #include "Toolkits/AssetEditorToolkit.h" +#include "Toolkits/AssetEditorManager.h" #include "LevelEditor.h" #include "GraphEditorModule.h" #include "AssetData.h" @@ -60,6 +61,7 @@ #include "Misc/MessageDialog.h" #include "UObject/ConstructorHelpers.h" #include "Misc/HotReloadInterface.h" +#include "Misc/ScopedSlowTask.h" #define LOCTEXT_NAMESPACE "AssetManagerEditor" @@ -297,6 +299,7 @@ public: void DumpAssetDependencies(const TArray& Args); virtual void OpenAssetAuditUI(TArray SelectedAssets) override; + virtual void OpenAssetAuditUI(TArray SelectedIdentifiers) override; virtual void OpenAssetAuditUI(TArray SelectedPackages) override; virtual void OpenReferenceViewerUI(TArray SelectedIdentifiers) override; virtual void OpenReferenceViewerUI(TArray SelectedPackages) override; @@ -352,8 +355,6 @@ private: FDelegateHandle ReferenceViewerDelegateHandle; FDelegateHandle AssetEditorExtenderDelegateHandle; FDelegateHandle LevelEditorExtenderDelegateHandle; - FDelegateHandle HotReloadDelegateHandle; - FDelegateHandle MarkPackageDirtyDelegateHandle; TWeakPtr AssetAuditTab; TWeakPtr ReferenceViewerTab; @@ -379,6 +380,7 @@ private: TSharedRef OnExtendLevelEditor(const TSharedRef CommandList, const TArray SelectedActors); void OnHotReload(bool bWasTriggeredAutomatically); void OnMarkPackageDirty(UPackage* Pkg, bool bWasDirty); + void OnEditAssetIdentifiers(TArray AssetIdentifiers); TSharedRef SpawnAssetAuditTab(const FSpawnTabArgs& Args); TSharedRef SpawnReferenceViewerTab(const FSpawnTabArgs& Args); @@ -499,9 +501,15 @@ void FAssetManagerEditorModule::StartupModule() // Register for hot reload and package dirty to invalidate data IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked("HotReload"); - HotReloadDelegateHandle = HotReloadSupport.OnHotReload().AddRaw(this, &FAssetManagerEditorModule::OnHotReload); + HotReloadSupport.OnHotReload().AddRaw(this, &FAssetManagerEditorModule::OnHotReload); - MarkPackageDirtyDelegateHandle = UPackage::PackageMarkedDirtyEvent.AddRaw(this, &FAssetManagerEditorModule::OnMarkPackageDirty); + UPackage::PackageMarkedDirtyEvent.AddRaw(this, &FAssetManagerEditorModule::OnMarkPackageDirty); + + // Register view callbacks + FEditorDelegates::OnOpenReferenceViewer.AddRaw(this, &FAssetManagerEditorModule::OpenReferenceViewerUI); + FEditorDelegates::OnOpenSizeMap.AddRaw(this, &FAssetManagerEditorModule::OpenSizeMapUI); + FEditorDelegates::OnOpenAssetAudit.AddRaw(this, &FAssetManagerEditorModule::OpenAssetAuditUI); + FEditorDelegates::OnEditAssetIdentifiers.AddRaw(this, &FAssetManagerEditorModule::OnEditAssetIdentifiers); } } @@ -584,10 +592,14 @@ void FAssetManagerEditorModule::ShutdownModule() if (FModuleManager::Get().IsModuleLoaded("HotReload")) { IHotReloadInterface& HotReloadSupport = FModuleManager::GetModuleChecked("HotReload"); - HotReloadSupport.OnHotReload().Remove(HotReloadDelegateHandle); + HotReloadSupport.OnHotReload().RemoveAll(this); } - UPackage::PackageMarkedDirtyEvent.Remove(MarkPackageDirtyDelegateHandle); + UPackage::PackageMarkedDirtyEvent.RemoveAll(this); + FEditorDelegates::OnOpenReferenceViewer.RemoveAll(this); + FEditorDelegates::OnOpenSizeMap.RemoveAll(this); + FEditorDelegates::OnOpenAssetAudit.RemoveAll(this); + FEditorDelegates::OnEditAssetIdentifiers.RemoveAll(this); } } @@ -640,6 +652,16 @@ void FAssetManagerEditorModule::OpenAssetAuditUI(TArray SelectedAsse } } +void FAssetManagerEditorModule::OpenAssetAuditUI(TArray SelectedIdentifiers) +{ + FGlobalTabmanager::Get()->InvokeTab(AssetAuditTabName); + + if (AssetAuditUI.IsValid()) + { + AssetAuditUI.Pin()->AddAssetsToList(SelectedIdentifiers, false); + } +} + void FAssetManagerEditorModule::OpenAssetAuditUI(TArray SelectedPackages) { FGlobalTabmanager::Get()->InvokeTab(AssetAuditTabName); @@ -916,6 +938,36 @@ void FAssetManagerEditorModule::OnMarkPackageDirty(UPackage* Pkg, bool bWasDirty } } +void FAssetManagerEditorModule::OnEditAssetIdentifiers(TArray AssetIdentifiers) +{ + // Handle default package behavior + TArray AssetsToLoad; + for (FAssetIdentifier AssetIdentifier : AssetIdentifiers) + { + if (AssetIdentifier.IsPackage()) + { + TArray Assets; + AssetRegistry->GetAssetsByPackageName(AssetIdentifier.PackageName, AssetsToLoad); + } + } + + if (AssetsToLoad.Num() > 0) + { + FScopedSlowTask SlowTask(0, LOCTEXT("LoadingSelectedObject", "Editing assets...")); + SlowTask.MakeDialogDelayed(.1f); + FAssetEditorManager& EditorManager = FAssetEditorManager::Get(); + + for (const FAssetData& AssetData : AssetsToLoad) + { + UObject* EditObject = AssetData.GetAsset(); + if (EditObject) + { + EditorManager.OpenEditorForAsset(EditObject); + } + } + } +} + bool FAssetManagerEditorModule::GetManagedPackageListForAssetData(const FAssetData& AssetData, TSet& ManagedPackageSet) { InitializeRegistrySources(true); diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.cpp b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.cpp index 2a13b5d157de..66204d912dee 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.cpp +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.cpp @@ -25,7 +25,6 @@ #include "ICollectionManager.h" #include "CollectionManagerModule.h" #include "Editor.h" -#include "Toolkits/AssetEditorManager.h" #include "AssetManagerEditorCommands.h" #include "EditorWidgetsModule.h" #include "Toolkits/GlobalEditorCommonCommands.h" @@ -907,7 +906,7 @@ void SReferenceViewer::RegisterActions() ReferenceViewerActions->MapAction( FAssetManagerEditorCommands::Get().OpenSelectedInAssetEditor, FExecuteAction::CreateSP(this, &SReferenceViewer::OpenSelectedInAssetEditor), - FCanExecuteAction::CreateSP(this, &SReferenceViewer::HasExactlyOnePackageNodeSelected)); + FCanExecuteAction::CreateSP(this, &SReferenceViewer::HasAtLeastOneRealNodeSelected)); ReferenceViewerActions->MapAction( FAssetManagerEditorCommands::Get().ReCenterGraph, @@ -972,12 +971,12 @@ void SReferenceViewer::RegisterActions() ReferenceViewerActions->MapAction( FAssetManagerEditorCommands::Get().ViewSizeMap, FExecuteAction::CreateSP(this, &SReferenceViewer::ViewSizeMap), - FCanExecuteAction()); + FCanExecuteAction::CreateSP(this, &SReferenceViewer::HasAtLeastOneRealNodeSelected)); ReferenceViewerActions->MapAction( FAssetManagerEditorCommands::Get().ViewAssetAudit, FExecuteAction::CreateSP(this, &SReferenceViewer::ViewAssetAudit), - FCanExecuteAction()); + FCanExecuteAction::CreateSP(this, &SReferenceViewer::HasAtLeastOneRealNodeSelected)); } void SReferenceViewer::ShowSelectionInContentBrowser() @@ -1005,29 +1004,22 @@ void SReferenceViewer::ShowSelectionInContentBrowser() void SReferenceViewer::OpenSelectedInAssetEditor() { + TArray IdentifiersToEdit; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TSet SelectedNodes = GraphEditorPtr->GetSelectedNodes(); for (FGraphPanelSelectionSet::TConstIterator It(SelectedNodes); It; ++It) { if (UEdGraphNode_Reference* ReferenceNode = Cast(*It)) { - if (!ReferenceNode->IsPackage() && !ReferenceNode->IsCollapsed()) + if (!ReferenceNode->IsCollapsed()) { - if (AssetRegistryModule.Get().EditSearchableName(ReferenceNode->GetIdentifier())) - { - // Was handled by callback, otherwise fall back to default behavior - return; - } + ReferenceNode->GetAllIdentifiers(IdentifiersToEdit); } } } - UObject* SelectedObject = GetObjectFromSingleSelectedNode(); - - if( SelectedObject ) - { - FAssetEditorManager::Get().OpenEditorForAsset(SelectedObject); - } + // This will handle packages as well as searchable names if other systems register + FEditorDelegates::OnEditAssetIdentifiers.Broadcast(IdentifiersToEdit); } void SReferenceViewer::ReCenterGraph() @@ -1406,6 +1398,27 @@ bool SReferenceViewer::HasAtLeastOnePackageNodeSelected() const return false; } +bool SReferenceViewer::HasAtLeastOneRealNodeSelected() const +{ + if (GraphEditorPtr.IsValid()) + { + TSet SelectedNodes = GraphEditorPtr->GetSelectedNodes(); + for (UObject* Node : SelectedNodes) + { + UEdGraphNode_Reference* ReferenceNode = Cast(Node); + if (ReferenceNode) + { + if (!ReferenceNode->IsCollapsed()) + { + return true; + } + } + } + } + + return false; +} + void SReferenceViewer::OnInitialAssetRegistrySearchComplete() { if ( GraphObj ) diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.h b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.h index 0f85e681e6b1..a909a1991f60 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.h +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/ReferenceViewer/SReferenceViewer.h @@ -141,6 +141,7 @@ private: bool HasExactlyOneNodeSelected() const; bool HasExactlyOnePackageNodeSelected() const; bool HasAtLeastOnePackageNodeSelected() const; + bool HasAtLeastOneRealNodeSelected() const; void OnInitialAssetRegistrySearchComplete(); EActiveTimerReturnType TriggerZoomToFit(double InCurrentTime, float InDeltaTime); diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.cpp b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.cpp index dc48d0464645..f077d9f3ffc4 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.cpp +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.cpp @@ -564,6 +564,22 @@ void SAssetAuditBrowser::AddAssetsToList(const TArray& AssetsToView, AddAssetsToList(AssetNames, bReplaceExisting); } +void SAssetAuditBrowser::AddAssetsToList(const TArray& AssetsToView, bool bReplaceExisting) +{ + TArray AssetNames; + + for (const FAssetIdentifier& AssetToView : AssetsToView) + { + FName PackageName = AssetToView.PackageName; + if (!PackageName.IsNone()) + { + AssetNames.AddUnique(PackageName); + } + } + + AddAssetsToList(AssetNames, bReplaceExisting); +} + void SAssetAuditBrowser::AddAssetsToList(const TArray& PackageNamesToView, bool bReplaceExisting) { AssetHistory.Insert(TSet(), ++CurrentAssetHistoryIndex); diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.h b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.h index 6c3fc52e84a3..b1558b2a50eb 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.h +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Private/SAssetAuditBrowser.h @@ -33,6 +33,7 @@ public: /** Adds assets to current management view */ void AddAssetsToList(const TArray& AssetsToView, bool bReplaceExisting); void AddAssetsToList(const TArray& AssetsToView, bool bReplaceExisting); + void AddAssetsToList(const TArray& AssetsToView, bool bReplaceExisting); void AddAssetsToList(const TArray& PackageNamesToView, bool bReplaceExisting); /** Called when the current registry source changes */ diff --git a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Public/AssetManagerEditorModule.h b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Public/AssetManagerEditorModule.h index ffcd36b99c63..72cfc35c92e6 100644 --- a/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Public/AssetManagerEditorModule.h +++ b/Engine/Plugins/Editor/AssetManagerEditor/Source/AssetManagerEditor/Public/AssetManagerEditorModule.h @@ -186,6 +186,7 @@ public: /** Opens asset management UI, with selected assets. Pass as value so it can be used in delegates */ virtual void OpenAssetAuditUI(TArray SelectedAssets) = 0; + virtual void OpenAssetAuditUI(TArray SelectedIdentifiers) = 0; virtual void OpenAssetAuditUI(TArray SelectedPackages) = 0; /** Spawns reference viewer, showing selected packages or identifiers */ diff --git a/Engine/Plugins/Editor/BlueprintMaterialTextureNodes/Source/BlueprintMaterialTextureNodes/Public/BlueprintMaterialTextureNodesBPLibrary.h b/Engine/Plugins/Editor/BlueprintMaterialTextureNodes/Source/BlueprintMaterialTextureNodes/Public/BlueprintMaterialTextureNodesBPLibrary.h index 6d108db6d405..db8cbc7e0921 100644 --- a/Engine/Plugins/Editor/BlueprintMaterialTextureNodes/Source/BlueprintMaterialTextureNodes/Public/BlueprintMaterialTextureNodesBPLibrary.h +++ b/Engine/Plugins/Editor/BlueprintMaterialTextureNodes/Source/BlueprintMaterialTextureNodes/Public/BlueprintMaterialTextureNodesBPLibrary.h @@ -33,21 +33,21 @@ class UBlueprintMaterialTextureNodesBPLibrary : public UBlueprintFunctionLibrary * Samples a texel from a Texture 2D with VectorDisplacement Compression */ UFUNCTION(BlueprintPure, meta = (DisplayName = "Texture2D Sample UV Editor Only", Keywords = "Read Texture UV"), Category = Rendering) - static FLinearColor Texture2D_SampleUV_EditorOnly(UTexture2D* Texture, FVector2D UV); + static FLinearColor Texture2D_SampleUV_EditorOnly(UTexture2D* Texture, FVector2D UV); /** * Samples an array of values from a Texture Render Target 2D. Currently only 4 channel formats are supported. * Only works in the editor */ UFUNCTION(BlueprintPure, meta = (DisplayName = "Render Target Sample Rectangle Editor Only", Keywords = "Sample Render Target Rectangle"), Category = Rendering) - static TArray RenderTarget_SampleRectangle_EditorOnly(UTextureRenderTarget2D* InRenderTarget, FLinearColor InRect); + static TArray RenderTarget_SampleRectangle_EditorOnly(UTextureRenderTarget2D* InRenderTarget, FLinearColor InRect); /** * Samples a value from a Texture Render Target 2D. Currently only 4 channel formats are supported. * Only works in the editor */ UFUNCTION(BlueprintPure, meta = (DisplayName = "Render Target Sample UV Editor Only", Keywords = "Sample Render Target UV"), Category = Rendering) - static FLinearColor RenderTarget_SampleUV_EditorOnly(UTextureRenderTarget2D* InRenderTarget, FVector2D UV); + static FLinearColor RenderTarget_SampleUV_EditorOnly(UTextureRenderTarget2D* InRenderTarget, FVector2D UV); /** @@ -55,57 +55,57 @@ class UBlueprintMaterialTextureNodesBPLibrary : public UBlueprintFunctionLibrary * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Create MIC Editor Only", Keywords = "Create MIC", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static UMaterialInstanceConstant* CreateMIC_EditorOnly(UMaterialInterface* Material, FString Name = "MIC_"); + static UMaterialInstanceConstant* CreateMIC_EditorOnly(UMaterialInterface* Material, FString Name = "MIC_"); UFUNCTION() - static void UpdateMIC(UMaterialInstanceConstant* MIC); + static void UpdateMIC(UMaterialInstanceConstant* MIC); /** * Sets a Scalar Parameter value in a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Scalar Parameter Editor Only", Keywords = "Set MIC Scalar Parameter", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICScalarParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName = "test", float Value = 0.0f); + static bool SetMICScalarParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName = "test", float Value = 0.0f); /** * Sets a Vector Parameter value in a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Vector Parameter Editor Only", Keywords = "Set MIC Vector Parameter", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICVectorParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName, FLinearColor Value = FLinearColor(0, 0, 0, 0)); + static bool SetMICVectorParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName, FLinearColor Value = FLinearColor(0, 0, 0, 0)); /** * Sets a Texture Parameter value in a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Texture Parameter Editor Only", Keywords = "Set MIC Texture Parameter", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICTextureParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName, UTexture2D* Texture = nullptr); + static bool SetMICTextureParam_EditorOnly(UMaterialInstanceConstant* Material, FString ParamName, UTexture2D* Texture = nullptr); /** * Overrides the Shading Model of a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Shading Model Editor Only", Keywords = "Set MIC Shading Model", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICShadingModel_EditorOnly(UMaterialInstanceConstant* Material, TEnumAsByte ShadingModel = MSM_DefaultLit); + static bool SetMICShadingModel_EditorOnly(UMaterialInstanceConstant* Material, TEnumAsByte ShadingModel = MSM_DefaultLit); /** * Overrides the Blend Mode of a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Blend Mode Editor Only", Keywords = "Set MIC Blend Mode", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICBlendMode_EditorOnly(UMaterialInstanceConstant* Material, TEnumAsByte BlendMode = BLEND_Opaque); + static bool SetMICBlendMode_EditorOnly(UMaterialInstanceConstant* Material, TEnumAsByte BlendMode = BLEND_Opaque); /** * Overrides the Two Sided setting of a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Two Sided Editor Only", Keywords = "Set MIC Two Sided", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICTwoSided_EditorOnly(UMaterialInstanceConstant* Material, bool TwoSided = false); + static bool SetMICTwoSided_EditorOnly(UMaterialInstanceConstant* Material, bool TwoSided = false); /** * Overrides the Blend Mode of a Material Instance Constant * Only works in the editor */ UFUNCTION(BlueprintCallable, meta = (DisplayName = "Set MIC Dithered LOD Editor Only", Keywords = "Set MIC Dithered LOD Transition", UnsafeDuringActorConstruction = "true"), Category = Rendering) - static bool SetMICDitheredLODTransition_EditorOnly(UMaterialInstanceConstant* Material, bool DitheredLODTransition = false); + static bool SetMICDitheredLODTransition_EditorOnly(UMaterialInstanceConstant* Material, bool DitheredLODTransition = false); }; diff --git a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Private/EditorSkeletalMeshLibrary.cpp b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Private/EditorSkeletalMeshLibrary.cpp index 8f61b421ad79..7c4b6cf651c1 100644 --- a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Private/EditorSkeletalMeshLibrary.cpp +++ b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Private/EditorSkeletalMeshLibrary.cpp @@ -8,7 +8,7 @@ #include "Engine/SkeletalMesh.h" #include "LODUtilities.h" -bool UEditorSkeletalMeshLibrary::RegenerateLOD(USkeletalMesh* SkeletalMesh, int32 NewLODCount /*= 0*/, bool bRegenerateEvenIfImported /*= false*/) +bool UEditorSkeletalMeshLibrary::RegenerateLOD(USkeletalMesh* SkeletalMesh, int32 NewLODCount /*= 0*/, bool bRegenerateEvenIfImported /*= false*/, bool bGenerateBaseLOD /*= false*/) { TGuardValue UnattendedScriptGuard(GIsRunningUnattendedScript, true); @@ -23,7 +23,7 @@ bool UEditorSkeletalMeshLibrary::RegenerateLOD(USkeletalMesh* SkeletalMesh, int3 return false; } - return FLODUtilities::RegenerateLOD(SkeletalMesh, NewLODCount, bRegenerateEvenIfImported); + return FLODUtilities::RegenerateLOD(SkeletalMesh, NewLODCount, bRegenerateEvenIfImported, bGenerateBaseLOD); } int32 UEditorSkeletalMeshLibrary::GetNumVerts(USkeletalMesh* SkeletalMesh, int32 LODIndex) diff --git a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorFilterLibrary.h b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorFilterLibrary.h index 28c04c36c71b..5272a605db0c 100644 --- a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorFilterLibrary.h +++ b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorFilterLibrary.h @@ -40,9 +40,9 @@ public: * @return The filtered list. */ UFUNCTION(BlueprintCallable, Category = "Editor Scripting | Utilities | Filter", meta = (DisplayName="Filter by Class", DeterminesOutputType = "ObjectClass")) - static TArray ByClass(const TArray& TargetArray - , TSubclassOf ObjectClass - , EEditorScriptingFilterType FilterType = EEditorScriptingFilterType::Include); + static TArray ByClass(const TArray& TargetArray + , TSubclassOf ObjectClass + , EEditorScriptingFilterType FilterType = EEditorScriptingFilterType::Include); /** * Filter the array based on the Object's ID name. diff --git a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorSkeletalMeshLibrary.h b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorSkeletalMeshLibrary.h index 95592a78e5c7..cecf439c297d 100644 --- a/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorSkeletalMeshLibrary.h +++ b/Engine/Plugins/Editor/EditorScriptingUtilities/Source/EditorScriptingUtilities/Public/EditorSkeletalMeshLibrary.h @@ -26,10 +26,11 @@ public: * Otherwise, it will use the current LOD and regenerate * @param bRegenerateEvenIfImported If this is true, it only regenerate even if this LOD was imported before * If false, it will regenerate for only previously auto generated ones + * @param bGenerateBaseLOD If this is true and there is some reduction data, the base LOD will be reduce according to the settings * @return true if succeed. If mesh reduction is not available this will return false. */ UFUNCTION(BlueprintCallable, Category = "Editor Scripting | SkeletalMesh", meta = (ScriptMethod)) - static bool RegenerateLOD(USkeletalMesh* SkeletalMesh, int32 NewLODCount = 0, bool bRegenerateEvenIfImported = false); + static bool RegenerateLOD(USkeletalMesh* SkeletalMesh, int32 NewLODCount = 0, bool bRegenerateEvenIfImported = false, bool bGenerateBaseLOD = false); /** Get number of mesh vertices for an LOD of a Skeletal Mesh * diff --git a/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimation/Private/AudioCurveSourceComponent.cpp b/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimation/Private/AudioCurveSourceComponent.cpp index 4119cb65797c..ee90bd38904a 100644 --- a/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimation/Private/AudioCurveSourceComponent.cpp +++ b/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimation/Private/AudioCurveSourceComponent.cpp @@ -44,10 +44,9 @@ void UAudioCurveSourceComponent::CacheCurveData() // cache audio sync curve static const FName AudioSyncCurve(TEXT("Audio")); - FRichCurve** RichCurvePtr = CurveData->RowMap.Find(AudioSyncCurve); - if (RichCurvePtr != nullptr && *RichCurvePtr != nullptr) + if (FRealCurve* Curve = CurveData->FindCurve(AudioSyncCurve, FString(), false)) { - CachedSyncPreRoll = (*RichCurvePtr)->GetFirstKey().Time; + CachedSyncPreRoll = Curve->GetKeyTime(Curve->GetFirstKeyHandle()); } CachedDuration = Sound->Duration; @@ -158,10 +157,9 @@ float UAudioCurveSourceComponent::GetCurveValue_Implementation(FName CurveName) UCurveTable* CurveTable = CachedCurveTable.Get(); if (CurveTable) { - FRichCurve** Curve = CurveTable->RowMap.Find(CurveName); - if (Curve != nullptr && *Curve != nullptr) + if (FRealCurve* Curve = CurveTable->FindCurve(CurveName, FString(), false)) { - return (*Curve)->Eval(CachedCurveEvalTime); + return Curve->Eval(CachedCurveEvalTime); } } } @@ -176,15 +174,15 @@ void UAudioCurveSourceComponent::GetCurves_Implementation(TArrayRowMap.Num()); + OutCurve.Reset(CurveTable->GetRowMap().Num()); if (bCachedLooping && CachedSyncPreRoll > 0.0f && Delay >= CachedSyncPreRoll && CachedCurveEvalTime >= CachedDuration - CachedSyncPreRoll) { // if we are looping and we have some preroll delay, we need to evaluate the curve twice // as we need to incorporate the preroll in the loop - for (auto Iter = CurveTable->RowMap.CreateConstIterator(); Iter; ++Iter) + for (auto Iter = CurveTable->GetRowMap().CreateConstIterator(); Iter; ++Iter) { - FRichCurve* Curve = Iter.Value(); + FRealCurve* Curve = Iter.Value(); float StandardValue = Curve->Eval(CachedCurveEvalTime); float LoopedValue = Curve->Eval(FMath::Fmod(CachedCurveEvalTime, CachedDuration)); @@ -194,7 +192,7 @@ void UAudioCurveSourceComponent::GetCurves_Implementation(TArrayRowMap.CreateConstIterator(); Iter; ++Iter) + for (auto Iter = CurveTable->GetRowMap().CreateConstIterator(); Iter; ++Iter) { OutCurve.Add({ Iter.Key(), Iter.Value()->Eval(CachedCurveEvalTime) }); } diff --git a/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimationEditor/Private/FacialAnimationImportItem.cpp b/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimationEditor/Private/FacialAnimationImportItem.cpp index 224627ff7808..4f056629c546 100644 --- a/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimationEditor/Private/FacialAnimationImportItem.cpp +++ b/Engine/Plugins/Editor/FacialAnimation/Source/FacialAnimationEditor/Private/FacialAnimationImportItem.cpp @@ -4,7 +4,7 @@ #include "FacialAnimationBulkImporterSettings.h" #include "Factories/SoundFactory.h" #include "Sound/SoundWave.h" -#include "Curves/RichCurve.h" +#include "Curves/SimpleCurve.h" #include "Engine/CurveTable.h" #include "FbxAnimUtils.h" #include "DesktopPlatformModule.h" @@ -89,8 +89,8 @@ bool FFacialAnimationImportItem::ImportCurvesEmbeddedInSoundWave() if (FbxAnimUtils::ImportCurveTableFromNode(FbxFile, GetDefault()->CurveNodeName, NewCurveTable, PreRollTime)) { // we will need to add a curve to tell us the time we want to start playing audio - FRichCurve* AudioCurve = NewCurveTable->RowMap.Add(TEXT("Audio"), new FRichCurve()); - AudioCurve->AddKey(PreRollTime, 1.0f); + FSimpleCurve& AudioCurve = NewCurveTable->AddSimpleCurve(TEXT("Audio")); + AudioCurve.AddKey(PreRollTime, 1.0f); return true; } diff --git a/Engine/Plugins/Editor/GameplayTagsEditor/GameplayTagsEditor.uplugin b/Engine/Plugins/Editor/GameplayTagsEditor/GameplayTagsEditor.uplugin index 9907b36ef002..d35a1e8282d3 100644 --- a/Engine/Plugins/Editor/GameplayTagsEditor/GameplayTagsEditor.uplugin +++ b/Engine/Plugins/Editor/GameplayTagsEditor/GameplayTagsEditor.uplugin @@ -21,12 +21,5 @@ "Type" : "Developer", "LoadingPhase" : "PreDefault" } - ], - "Plugins": - [ - { - "Name" : "AssetManagerEditor", - "Enabled" : true - } ] } \ No newline at end of file diff --git a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/GameplayTagsEditor.Build.cs b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/GameplayTagsEditor.Build.cs index 2834e63469e0..b6e8d89ce58c 100644 --- a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/GameplayTagsEditor.Build.cs +++ b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/GameplayTagsEditor.Build.cs @@ -34,8 +34,7 @@ namespace UnrealBuildTool.Rules "ContentBrowser", "MainFrame", "UnrealEd", - "SourceControl", - "AssetManagerEditor" + "SourceControl" } ); diff --git a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/GameplayTagsEditorModule.cpp b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/GameplayTagsEditorModule.cpp index 5762277777b9..be5d316d0153 100644 --- a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/GameplayTagsEditorModule.cpp +++ b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/GameplayTagsEditorModule.cpp @@ -81,13 +81,11 @@ public: GameplayTagPackageName = FGameplayTag::StaticStruct()->GetOutermost()->GetFName(); GameplayTagStructName = FGameplayTag::StaticStruct()->GetFName(); - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); - AssetRegistryModule.Get().OnEditSearchableName(GameplayTagPackageName, GameplayTagStructName).BindRaw(this, &FGameplayTagsEditorModule::OnEditGameplayTag); - // Hook into notifications for object re-imports so that the gameplay tag tree can be reconstructed if the table changes if (GIsEditor) { FEditorDelegates::OnAssetPostImport.AddRaw(this, &FGameplayTagsEditorModule::OnObjectReimported); + FEditorDelegates::OnEditAssetIdentifiers.AddRaw(this, &FGameplayTagsEditorModule::OnEditGameplayTag); IGameplayTagsModule::OnTagSettingsChanged.AddRaw(this, &FGameplayTagsEditorModule::OnEditorSettingsChanged); UPackage::PackageSavedEvent.AddRaw(this, &FGameplayTagsEditorModule::OnPackageSaved); } @@ -116,14 +114,9 @@ public: } FEditorDelegates::OnAssetPostImport.RemoveAll(this); + FEditorDelegates::OnEditAssetIdentifiers.RemoveAll(this); IGameplayTagsModule::OnTagSettingsChanged.RemoveAll(this); UPackage::PackageSavedEvent.RemoveAll(this); - - FAssetRegistryModule* AssetRegistryModule = FModuleManager::FModuleManager::GetModulePtr("AssetRegistry"); - if (AssetRegistryModule) - { - AssetRegistryModule->Get().OnEditSearchableName(GameplayTagPackageName, GameplayTagStructName).Unbind(); - } } void OnEditorSettingsChanged() @@ -166,15 +159,21 @@ public: } } - bool OnEditGameplayTag(const FAssetIdentifier& AssetId) + void OnEditGameplayTag(TArray AssetIdentifierList) { - if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + // If any of these are gameplay tags, open up tag viewer + for (FAssetIdentifier Identifier : AssetIdentifierList) { - // TODO: Select tag maybe? - SettingsModule->ShowViewer("Project", "Project", "GameplayTags"); + if (Identifier.IsValue() && Identifier.PackageName == GameplayTagPackageName && Identifier.ObjectName == GameplayTagStructName) + { + if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + // TODO: Select tag maybe? + SettingsModule->ShowViewer("Project", "Project", "GameplayTags"); + } + return; + } } - - return true; } void ShowNotification(const FText& TextToDisplay, float TimeToDisplay) diff --git a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.cpp b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.cpp index 79e2d25a1803..76b4f7ae9509 100644 --- a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.cpp +++ b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.cpp @@ -29,7 +29,7 @@ #include "SAddNewRestrictedGameplayTagWidget.h" #include "SRenameGameplayTagDialog.h" #include "AssetData.h" -#include "AssetManagerEditorModule.h" +#include "Editor.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" @@ -710,6 +710,32 @@ ECheckBoxState SGameplayTagWidget::IsTagChecked(TSharedPtr Nod } } +bool SGameplayTagWidget::IsExactTagInCollection(TSharedPtr Node) const +{ + if (Node.IsValid()) + { + UGameplayTagsManager& TagsManager = UGameplayTagsManager::Get(); + + for (int32 ContainerIdx = 0; ContainerIdx < TagContainers.Num(); ++ContainerIdx) + { + FGameplayTagContainer* Container = TagContainers[ContainerIdx].TagContainer; + if (Container) + { + FGameplayTag GameplayTag = Node->GetCompleteTag(); + if (GameplayTag.IsValid()) + { + if (Container->HasTagExact(GameplayTag)) + { + return true; + } + } + } + } + } + + return false; +} + void SGameplayTagWidget::OnAllowChildrenTagCheckStatusChanged(ECheckBoxState NewCheckState, TSharedPtr NodeChanged) { IGameplayTagsEditorModule& TagsEditor = IGameplayTagsEditorModule::Get(); @@ -877,13 +903,22 @@ TSharedRef SGameplayTagWidget::MakeTagActionsMenu(TSharedPtr InTagNode) } } +void SGameplayTagWidget::OnAddTag(TSharedPtr InTagNode) +{ + if (InTagNode.IsValid()) + { + for (int32 ContainerIdx = 0; ContainerIdx < TagContainers.Num(); ++ContainerIdx) + { + FGameplayTagContainer* Container = TagContainers[ContainerIdx].TagContainer; + Container->AddTag(InTagNode->GetCompleteTag()); + } + + OnTagChanged.ExecuteIfBound(); + } +} + +void SGameplayTagWidget::OnRemoveTag(TSharedPtr InTagNode) +{ + if (InTagNode.IsValid()) + { + for (int32 ContainerIdx = 0; ContainerIdx < TagContainers.Num(); ++ContainerIdx) + { + FGameplayTagContainer* Container = TagContainers[ContainerIdx].TagContainer; + Container->RemoveTag(InTagNode->GetCompleteTag()); + } + + OnTagChanged.ExecuteIfBound(); + } +} + void SGameplayTagWidget::OnSearchForReferences(TSharedPtr InTagNode) { - if (InTagNode.IsValid() && IAssetManagerEditorModule::IsAvailable()) + if (InTagNode.IsValid()) { - IAssetManagerEditorModule& ManagerEditorModule = IAssetManagerEditorModule::Get(); - TArray AssetIdentifiers; AssetIdentifiers.Add(FAssetIdentifier(FGameplayTag::StaticStruct(), InTagNode->GetCompleteTagName())); - - ManagerEditorModule.OpenReferenceViewerUI(AssetIdentifiers); + FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers); } } diff --git a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.h b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.h index e889a9c2776c..cfd01e200f89 100644 --- a/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.h +++ b/Engine/Plugins/Editor/GameplayTagsEditor/Source/GameplayTagsEditor/Private/SGameplayTagWidget.h @@ -203,6 +203,11 @@ private: */ ECheckBoxState IsTagChecked(TSharedPtr Node) const; + /** + * @return true if the exact Tag provided is included in any of the tag containers the widget is editing. + */ + bool IsExactTagInCollection(TSharedPtr Node) const; + /** * Called via delegate when the status of the allow non-restricted children check box in a row changes * @@ -332,6 +337,12 @@ private: /** Attempts to delete the specified tag */ void OnDeleteTag(TSharedPtr InTagNode); + /** Attempts to add the exact specified tag*/ + void OnAddTag(TSharedPtr InTagNode); + + /** Attempts to remove the specified tag, but not the children */ + void OnRemoveTag(TSharedPtr InTagNode); + /** Searches for all references for the selected tag */ void OnSearchForReferences(TSharedPtr InTagNode); diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/MaterialAnalyzer.uplugin b/Engine/Plugins/Editor/MaterialAnalyzer/MaterialAnalyzer.uplugin new file mode 100644 index 000000000000..d7a9f733fda3 --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/MaterialAnalyzer.uplugin @@ -0,0 +1,30 @@ +{ + "FileVersion": 1, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Material Analyzer", + "Description": "Analyzer to discover possible memory savings in material shaders.", + "Category": "Editor", + "CreatedBy": "Epic Games, Inc.", + "CreatedByURL": "http://epicgames.com", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "EnabledByDefault": true, + "CanContainContent": false, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "MaterialAnalyzer", + "Type": "Editor", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "AssetManagerEditor", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzer.Build.cs b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzer.Build.cs new file mode 100644 index 000000000000..49ab4a1e1eb7 --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzer.Build.cs @@ -0,0 +1,32 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +namespace UnrealBuildTool.Rules +{ + public class MaterialAnalyzer : ModuleRules + { + public MaterialAnalyzer(ReadOnlyTargetRules Target) : base(Target) + { + PublicIncludePaths.AddRange( + new string[] { + "Editor/WorkspaceMenuStructure/Public" + } + ); + PublicDependencyModuleNames.AddRange(new string[] { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "Slate", + "SlateCore", + "EditorStyle", + "UnrealEd", + "PropertyEditor" + }); + + PrivateDependencyModuleNames.AddRange(new string[] { + "AssetManagerEditor" + }); + } + } + +} \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.cpp b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.cpp new file mode 100644 index 000000000000..6d6c62418ef0 --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.cpp @@ -0,0 +1,68 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "MaterialAnalyzerModule.h" +#include "SMaterialAnalyzer.h" +#include "EditorModeRegistry.h" +#include "Modules/ModuleManager.h" +#include "Framework/Docking/TabManager.h" +#include "ISettingsModule.h" +#include "Editor.h" +#include "WorkspaceMenuStructure.h" +#include "WorkspaceMenuStructureModule.h" +#include "EditorStyleSet.h" +#include "Widgets/Docking/SDockTab.h" + +#define LOCTEXT_NAMESPACE "MaterialAnalyzer" + +DEFINE_LOG_CATEGORY(MaterialAnalyzer); + +static const FName MaterialAnalyzerName("MaterialAnalyzer"); + +class FMaterialAnalyzerModule : public IModuleInterface +{ +public: + FMaterialAnalyzerModule() + { + } + + // FModuleInterface overrides + virtual void StartupModule() override; + virtual void ShutdownModule() override {} + virtual bool SupportsDynamicReloading() override + { + return true; + } + + TSharedRef SpawnMaterialAnalyzerTab(const FSpawnTabArgs& SpawnTabArgs); + +protected: +}; + +void FMaterialAnalyzerModule::StartupModule() +{ + FGlobalTabmanager::Get()->RegisterNomadTabSpawner( + MaterialAnalyzerName, + FOnSpawnTab::CreateRaw(this, &FMaterialAnalyzerModule::SpawnMaterialAnalyzerTab)) + .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsMiscCategory()) + .SetDisplayName(LOCTEXT("TabTitle", "Material Analyzer")) + .SetTooltipText(LOCTEXT("TooltipText", "Opens Material Analyzer tool.")) + .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "MaterialEditor.ToggleMaterialStats.Tab")); +} + +TSharedRef FMaterialAnalyzerModule::SpawnMaterialAnalyzerTab(const FSpawnTabArgs& SpawnTabArgs) +{ + const TSharedRef MajorTab = SNew(SDockTab) + .TabRole(ETabRole::NomadTab); + + TSharedPtr TabContent; + + TabContent = SNew(SMaterialAnalyzer, MajorTab, SpawnTabArgs.GetOwnerWindow()); + + MajorTab->SetContent(TabContent.ToSharedRef()); + + return MajorTab; +} + +IMPLEMENT_MODULE(FMaterialAnalyzerModule, MaterialAnalyzer) + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.h b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.h new file mode 100644 index 000000000000..7b6642006daf --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/MaterialAnalyzerModule.h @@ -0,0 +1,7 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +DECLARE_LOG_CATEGORY_EXTERN(MaterialAnalyzer, Log, All); \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/AnalyzedMaterialNode.h b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/AnalyzedMaterialNode.h new file mode 100644 index 000000000000..dc9cbfddd9cf --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/AnalyzedMaterialNode.h @@ -0,0 +1,172 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +struct FBasePropertyOverrideNode +{ +public: + FBasePropertyOverrideNode(FName InParameterName, FName InParameterID, float InParameterValue, bool bInOverride) : + ParameterName(InParameterName), + ParameterID(InParameterID), + ParameterValue(InParameterValue), + bOverride(bInOverride) + { + + } + FName ParameterName; + FName ParameterID; + float ParameterValue; + bool bOverride; + + TArray>* Children; +}; + +struct FStaticMaterialLayerParameterNode +{ +public: + FStaticMaterialLayerParameterNode(FName InParameterName, FString InParameterValue, bool bInOverride): + ParameterName(InParameterName), + ParameterValue(InParameterValue), + bOverride(bInOverride) + { + + } + FName ParameterName; + FString ParameterValue; + bool bOverride; +}; + +struct FStaticSwitchParameterNode +{ +public: + FStaticSwitchParameterNode(FName InParameterName, bool InParameterValue, bool bInOverride) : + ParameterName(InParameterName), + ParameterValue(InParameterValue), + bOverride(bInOverride) + { + + } + FName ParameterName; + bool ParameterValue; + bool bOverride; + + TArray>* Children; +}; + +struct FStaticComponentMaskParameterNode +{ +public: + FStaticComponentMaskParameterNode(FName InParameterName, bool InR, bool InG, bool InB, bool InA, bool bInOverride) : + ParameterName(InParameterName), + R(InR), + G(InG), + B(InB), + A(InA), + bOverride(bInOverride) + { + + } + FName ParameterName; + bool R; + bool G; + bool B; + bool A; + bool bOverride; +}; + +typedef TSharedRef FBasePropertyOverrideNodeRef; + +typedef TSharedRef FStaticMaterialLayerParameterNodeRef; + +typedef TSharedRef FStaticSwitchParameterNodeRef; + +typedef TSharedRef FStaticComponentMaskParameterNodeRef; + +typedef TSharedRef FAnalyzedMaterialNodeRef; + +typedef TSharedPtr FAnalyzedMaterialNodePtr; + +struct FAnalyzedMaterialNode +{ +public: + /** + * Add the given node to our list of children for this material (this node will keep a strong reference to the instance) + */ + FAnalyzedMaterialNodeRef* AddChildNode(FAnalyzedMaterialNodeRef InChildNode) + { + ChildNodes.Add(InChildNode); + return &ChildNodes.Last(); + } + + /** + * @return The node entries for the material's children + */ + TArray& GetChildNodes() + { + return ChildNodes; + } + + TArray* GetChildNodesPtr() + { + return &ChildNodes; + } + + int ActualNumberOfChildren() const + { + return ChildNodes.Num(); + } + + int TotalNumberOfChildren() const + { + int32 TotalChildren = 0; + + for(int i = 0; i < ChildNodes.Num(); ++i) + { + TotalChildren += ChildNodes[i]->TotalNumberOfChildren(); + } + + return TotalChildren + ChildNodes.Num(); + } + + FBasePropertyOverrideNodeRef FindBasePropertyOverride(FName ParameterName) + { + FBasePropertyOverrideNodeRef* BasePropertyOverride = BasePropertyOverrides.FindByPredicate([&](const FBasePropertyOverrideNodeRef& Entry) { return Entry->ParameterName == ParameterName; }); + check(BasePropertyOverride != nullptr); + return *BasePropertyOverride; + } + + FStaticMaterialLayerParameterNodeRef FindMaterialLayerParameter(FName ParameterName) + { + FStaticMaterialLayerParameterNodeRef* MaterialLayerParameter = MaterialLayerParameters.FindByPredicate([&](const FStaticMaterialLayerParameterNodeRef& Entry) { return Entry->ParameterName == ParameterName; }); + check(MaterialLayerParameter != nullptr); + return *MaterialLayerParameter; + } + + FStaticSwitchParameterNodeRef FindStaticSwitchParameter(FName ParameterName) + { + FStaticSwitchParameterNodeRef* StaticSwitchParameter = StaticSwitchParameters.FindByPredicate([&](const FStaticSwitchParameterNodeRef& Entry) { return Entry->ParameterName == ParameterName; }); + check(StaticSwitchParameter != nullptr); + return *StaticSwitchParameter; + } + + FStaticComponentMaskParameterNodeRef FindStaticComponentMaskParameter(FName ParameterName) + { + FStaticComponentMaskParameterNodeRef* StaticComponentMaskParameter = StaticComponentMaskParameters.FindByPredicate([&](const FStaticComponentMaskParameterNodeRef& Entry) { return Entry->ParameterName == ParameterName; }); + check(StaticComponentMaskParameter != nullptr); + return *StaticComponentMaskParameter; + } + + FString Path; + FName ObjectPath; + FAnalyzedMaterialNodePtr Parent; + + TArray BasePropertyOverrides; + TArray MaterialLayerParameters; + TArray StaticSwitchParameters; + TArray StaticComponentMaskParameters; + +protected: + TArray ChildNodes; +}; \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.cpp b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.cpp new file mode 100644 index 000000000000..3f4041dda471 --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.cpp @@ -0,0 +1,396 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved + +#include "SAnalyzedMaterialNodeWidgetItem.h" +#include "Widgets/Views/STreeView.h" +#include "SlateOptMacros.h" +#include "Widgets/Images/SImage.h" +#include "Engine/EngineTypes.h" +#include "MaterialShaderType.h" + +#define LOCTEXT_NAMESPACE "MaterialAnalyzer" + +FName SAnalyzedMaterialNodeWidgetItem::NAME_MaterialName(TEXT("MaterialName")); +FName SAnalyzedMaterialNodeWidgetItem::NAME_NumberOfChildren(TEXT("MaterialChildren")); +FName SAnalyzedMaterialNodeWidgetItem::NAME_BasePropertyOverrides(TEXT("BasePropertyOverrides")); +FName SAnalyzedMaterialNodeWidgetItem::NAME_MaterialLayerParameters(TEXT("MaterialLayerParameters")); +FName SAnalyzedMaterialNodeWidgetItem::NAME_StaticSwitchParameters(TEXT("StaticSwitchParameters")); +FName SAnalyzedMaterialNodeWidgetItem::NAME_StaticComponentMaskParameters(TEXT("StaticComponentMaskParameters")); + +void SAnalyzedMaterialNodeWidgetItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) +{ + this->MaterialInfo = InArgs._MaterialInfoToVisualize; + + this->SetPadding(0); + + this->BasePropertyOverrideNodes = MaterialInfo->BasePropertyOverrides; + this->StaticSwitchNodes = MaterialInfo->StaticSwitchParameters; + this->StaticMaterialLayerNodes = MaterialInfo->MaterialLayerParameters; + this->StaticComponentMaskNodes = MaterialInfo->StaticComponentMaskParameters; + + CachedMaterialName = FText::FromString(MaterialInfo->Path); + TotalNumberOfChildren = MaterialInfo->TotalNumberOfChildren(); + NumberOfChildren = MaterialInfo->ActualNumberOfChildren(); + + SMultiColumnTableRow< FAnalyzedMaterialNodeRef >::Construct(SMultiColumnTableRow< FAnalyzedMaterialNodeRef >::FArguments().Padding(0), InOwnerTableView); +} + +TSharedRef SAnalyzedMaterialNodeWidgetItem::GenerateWidgetForColumn(const FName& ColumnName) +{ + if(ColumnName == NAME_MaterialName) + { + return SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Top) + [ + SNew(SExpanderArrow, SharedThis(this)) + .IndentAmount(16) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f, 0.0f) + .VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Text(this, &SAnalyzedMaterialNodeWidgetItem::GetMaterialName) + ]; + } + else if(ColumnName == NAME_NumberOfChildren) + { + return SNew(STextBlock) + .Text(this, &SAnalyzedMaterialNodeWidgetItem::GetNumberOfChildren) + .Justification(ETextJustify::Left); + } + else if (ColumnName == NAME_BasePropertyOverrides) + { + return SNew(SBasePropertyOverrideWidget) + .StaticInfos(BasePropertyOverrideNodes); + } + else if(ColumnName == NAME_MaterialLayerParameters) + { + return SNew(SStaticMaterialLayerParameterWidget) + .StaticInfos(StaticMaterialLayerNodes); + } + else if (ColumnName == NAME_StaticSwitchParameters) + { + return SNew(SStaticSwitchParameterWidget) + .StaticInfos(StaticSwitchNodes); + } + else if (ColumnName == NAME_StaticComponentMaskParameters) + { + return SNew(SStaticComponentMaskParameterWidget) + .StaticInfos(StaticComponentMaskNodes); + } + + return SNullWidget::NullWidget; +} + + + +template +void SStaticParameterWidget::Construct(const FArguments& InArgs) +{ + StaticNodes = InArgs._StaticInfos; + StyleSet = InArgs._StyleSet; + + DataVerticalBox = SNew(SVerticalBox).Visibility(EVisibility::Collapsed); + + for (int i = 0; i < StaticNodes.Num(); ++i) + { + if (!StaticNodes[i]->bOverride) + { + continue; + } + + DataVerticalBox->AddSlot() + [ + CreateRowWidget(StaticNodes[i]) + ]; + } + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SAssignNew(ExpanderButton, SButton) + .ButtonStyle(FCoreStyle::Get(), "NoBorder") + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ClickMethod(EButtonClickMethod::MouseDown) + .OnClicked(this, &SStaticParameterWidget::DoExpand) + .ContentPadding(0.f) + .ForegroundColor(FSlateColor::UseForeground()) + .IsFocusable(false) + [ + SNew(SImage) + .Image(this, &SStaticParameterWidget ::GetExpanderImage) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + ] + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Text(this, &SStaticParameterWidget::GetBaseText) + ] + ] + +SVerticalBox::Slot() + .AutoHeight() + [ + DataVerticalBox.ToSharedRef() + ] + ]; + + ExpanderButton->SetVisibility(DataVerticalBox->NumSlots() > 0 ? EVisibility::Visible : EVisibility::Hidden); +} + +template +FText SStaticParameterWidget::GetBaseText() const +{ + return LOCTEXT("NotOverridenErrorMessage", "Override SStaticParameterWidget::GetBaseText"); +} + + +FText SBasePropertyOverrideWidget::GetBaseText() const +{ + return FText::Format(FTextFormat(LOCTEXT("NumberOfBasePropertyOverrides", "{0} Base Property Overrides")), DataVerticalBox->NumSlots()); +} + +TSharedRef SBasePropertyOverrideWidget::CreateRowWidget(FBasePropertyOverrideNodeRef RowData) +{ + FText DisplayText = FText::GetEmpty(); + + if (RowData->ParameterID.IsEqual(TEXT("bOverride_OpacityMaskClipValue"))) + { + DisplayText = FText::AsNumber(RowData->ParameterValue); + } + else if (RowData->ParameterID.IsEqual(TEXT("bOverride_BlendMode"))) + { + int32 BlendID = (int32)RowData->ParameterValue; + DisplayText = FText::FromString(GetBlendModeString((EBlendMode)BlendID)); + } + else if (RowData->ParameterID.IsEqual(TEXT("bOverride_ShadingModel"))) + { + int32 BlendID = (int32)RowData->ParameterValue; + DisplayText = FText::FromString(GetShadingModelString((EMaterialShadingModel)BlendID)); + } + else // bool values + { + DisplayText = RowData->ParameterValue ? LOCTEXT("True", "True") : LOCTEXT("False", "False"); + } + + + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(24, 0, 0, 0) + [ + SNew(STextBlock) + .Text(FText::FromName(RowData->ParameterName)) + ] + + SHorizontalBox::Slot() + .Padding(2, 0, 0, 0) + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(DisplayText) + ]; +} + +FText SStaticSwitchParameterWidget::GetBaseText() const +{ + return FText::Format(FTextFormat(LOCTEXT("NumberOfStaticSwitchParameters", "{0} Static Switch Parameters")), DataVerticalBox->NumSlots()); +} + +TSharedRef SStaticSwitchParameterWidget::CreateRowWidget(FStaticSwitchParameterNodeRef RowData) +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(24, 0, 0, 0) + [ + SNew(STextBlock) + .Text(FText::FromName(RowData->ParameterName)) + ] + + SHorizontalBox::Slot() + .Padding(2, 0, 0, 0) + .AutoWidth() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(RowData->ParameterValue ? LOCTEXT("True", "True") : LOCTEXT("False", "False")) + ]; +} + +FText SStaticComponentMaskParameterWidget::GetBaseText() const +{ + return FText::Format(FTextFormat(LOCTEXT("NumberOfStaticComponentMaskParameters", "{0} Static Component Mask Parameters")), DataVerticalBox->NumSlots()); +} + + +TSharedRef SStaticComponentMaskParameterWidget::CreateRowWidget(FStaticComponentMaskParameterNodeRef RowData) +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(24, 0, 0, 0) + [ + SNew(STextBlock) + .Text(FText::FromName(RowData->ParameterName)) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 10, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("R"))) + ] + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(RowData->R ? LOCTEXT("True", "True") : LOCTEXT("False", "False")) + ] + ] + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 10, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("G"))) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(RowData->G ? LOCTEXT("True", "True") : LOCTEXT("False", "False")) + ] + ] + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 10, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("B"))) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(RowData->B ? LOCTEXT("True", "True") : LOCTEXT("False", "False")) + ] + ] + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0,0,10,0) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("A"))) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + [ + SNew(STextBlock) + .Text(RowData->A ? LOCTEXT("True", "True") : LOCTEXT("False", "False")) + ] + ] + ]; +} + +FText SStaticMaterialLayerParameterWidget::GetBaseText() const +{ + return FText::Format(FTextFormat(LOCTEXT("NumberOfStaticMaterialLayerParameters", "{0} Static Material Layer Parameters")), DataVerticalBox->NumSlots()); +} + +TSharedRef SStaticMaterialLayerParameterWidget::CreateRowWidget(FStaticMaterialLayerParameterNodeRef RowData) +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(24, 0, 0, 0) + [ + SNew(STextBlock) + .Text(FText::FromName(RowData->ParameterName)) + ] + + SHorizontalBox::Slot() + .Padding(2, 0, 0, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(RowData->ParameterValue)) + ]; + +} + +template +FReply SStaticParameterWidget::DoExpand() +{ + if(bIsExpanded) + { + DataVerticalBox->SetVisibility(EVisibility::Collapsed); + bIsExpanded = false; + } + else + { + DataVerticalBox->SetVisibility(EVisibility::Visible); + bIsExpanded = true; + } + return FReply::Handled(); +} + +/** @return the name of an image that should be shown as the expander arrow */ +template +const FSlateBrush* SStaticParameterWidget::GetExpanderImage() const +{ + FName ResourceName; + if (bIsExpanded) + { + if (ExpanderButton->IsHovered()) + { + static FName ExpandedHoveredName = "TreeArrow_Expanded_Hovered"; + ResourceName = ExpandedHoveredName; + } + else + { + static FName ExpandedName = "TreeArrow_Expanded"; + ResourceName = ExpandedName; + } + } + else + { + if (ExpanderButton->IsHovered()) + { + static FName CollapsedHoveredName = "TreeArrow_Collapsed_Hovered"; + ResourceName = CollapsedHoveredName; + } + else + { + static FName CollapsedName = "TreeArrow_Collapsed"; + ResourceName = CollapsedName; + } + } + + return StyleSet->GetBrush(ResourceName); +} + + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.h b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.h new file mode 100644 index 000000000000..1bddbe9949e9 --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SAnalyzedMaterialNodeWidgetItem.h @@ -0,0 +1,146 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Styling/SlateColor.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SWidget.h" +#include "Widgets/Views/STableViewBase.h" +#include "Widgets/Views/STableRow.h" +#include "Widgets/Views/SListView.h" +#include "AnalyzedMaterialNode.h" +#include "Widgets/Input/SButton.h" + +class SAnalyzedMaterialNodeWidgetItem + : public SMultiColumnTableRow +{ +public: + static FName NAME_MaterialName; + static FName NAME_NumberOfChildren; + static FName NAME_BasePropertyOverrides; + static FName NAME_MaterialLayerParameters; + static FName NAME_StaticSwitchParameters; + static FName NAME_StaticComponentMaskParameters; + + SLATE_BEGIN_ARGS(SAnalyzedMaterialNodeWidgetItem) + : _MaterialInfoToVisualize() + { } + + SLATE_ARGUMENT(FAnalyzedMaterialNodePtr, MaterialInfoToVisualize) + + SLATE_END_ARGS() + + +public: + + /** + * Construct child widgets that comprise this widget. + * + * @param InArgs Declaration from which to construct this widget. + */ + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView); + + + // SMultiColumnTableRow overrides + virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override; + + FText GetMaterialName() const + { + return CachedMaterialName; + } + + FText GetNumberOfChildren() const + { + return FText::Format(FTextFormat::FromString(TEXT("{0}/{1}")), NumberOfChildren, TotalNumberOfChildren); + } + +protected: + /** The info about the widget that we are visualizing. */ + FAnalyzedMaterialNodePtr MaterialInfo; + + FText CachedMaterialName; + int TotalNumberOfChildren; + int NumberOfChildren; + + TArray BasePropertyOverrideNodes; + TArray StaticSwitchNodes; + TArray StaticComponentMaskNodes; + TArray StaticMaterialLayerNodes; +}; + +template +class SStaticParameterWidget + : public SCompoundWidget +{ + SLATE_BEGIN_ARGS(SStaticParameterWidget) + : _StyleSet(&FCoreStyle::Get()), + _StaticInfos() + {} + + SLATE_ARGUMENT(const ISlateStyle*, StyleSet) + SLATE_ARGUMENT(TArray, StaticInfos) + + SLATE_END_ARGS() +public: + SStaticParameterWidget() : + bIsExpanded(false) + { + + } + + virtual TSharedRef CreateRowWidget(NodeType RowData) + { + return SNullWidget::NullWidget; + } + + virtual FText GetBaseText() const; + + void Construct(const FArguments& InArgs); + + TSharedPtr DataVerticalBox; + TSharedPtr ExpanderButton; + + TArray StaticNodes; + + FReply DoExpand(); + const FSlateBrush* GetExpanderImage() const; + + /** The slate style to use */ + const ISlateStyle* StyleSet; + + bool bIsExpanded; + +}; + +class SBasePropertyOverrideWidget + : public SStaticParameterWidget +{ + virtual TSharedRef CreateRowWidget(FBasePropertyOverrideNodeRef RowData) override; + + virtual FText GetBaseText() const override; +}; + +class SStaticSwitchParameterWidget + : public SStaticParameterWidget +{ + virtual TSharedRef CreateRowWidget(FStaticSwitchParameterNodeRef RowData) override; + + virtual FText GetBaseText() const override; +}; + +class SStaticComponentMaskParameterWidget + : public SStaticParameterWidget +{ + virtual TSharedRef CreateRowWidget(FStaticComponentMaskParameterNodeRef RowData) override; + + virtual FText GetBaseText() const override; +}; + +class SStaticMaterialLayerParameterWidget + : public SStaticParameterWidget +{ +public: + virtual TSharedRef CreateRowWidget(FStaticMaterialLayerParameterNodeRef RowData) override; + virtual FText GetBaseText() const override; +}; \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.cpp b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.cpp new file mode 100644 index 000000000000..cf21f8e55c8d --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.cpp @@ -0,0 +1,929 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "SMaterialAnalyzer.h" +#include "Widgets/Input/SSearchBox.h" +#include "AssetRegistryModule.h" +#include "Async/AsyncWork.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Styling/SlateIconFinder.h" +#include "Framework/Commands/UIAction.h" +#include "Materials/MaterialLayersFunctions.h" +#include "Hash/CityHash.h" +#include "EditorStyleSet.h" +#include "PropertyCustomizationHelpers.h" +#include "CollectionManagerTypes.h" +#include "CollectionManagerModule.h" +#include "ICollectionManager.h" +#include "AssetManagerEditorModule.h" +#include "Widgets/Images/SImage.h" + +#define LOCTEXT_NAMESPACE "MaterialAnalyzer" + +static TMap BasePropertyOverrideNames; + +SMaterialAnalyzer::SMaterialAnalyzer() + : BuildBaseMaterialTreeTask(nullptr) + , AnalyzeTreeTask(nullptr) + , AnalyzeForIdenticalPermutationsTask(nullptr) + , bRequestedTreeRefresh(false) + , bWaitingForAssetRegistryLoad(false) +{ + BasePropertyOverrideNames.Empty(); + BasePropertyOverrideNames.Add(TEXT("bOverride_OpacityMaskClipValue"), TEXT("OpacityMaskClipValueOverride")); + BasePropertyOverrideNames.Add(TEXT("bOverride_BlendMode"), TEXT("BlendModeOverride")); + BasePropertyOverrideNames.Add(TEXT("bOverride_ShadingModel"), TEXT("ShadingModelOverride")); + BasePropertyOverrideNames.Add(TEXT("bOverride_DitheredLODTransition"), TEXT("DitheredLODTransitionOverride")); + BasePropertyOverrideNames.Add(TEXT("bOverride_CastDynamicShadowAsMasked"), TEXT("CastDynamicShadowAsMaskedOverride")); + BasePropertyOverrideNames.Add(TEXT("bOverride_TwoSided"), TEXT("TwoSidedOverride")); +} + +SMaterialAnalyzer::~SMaterialAnalyzer() +{ + +} + +const FAssetData* FindParentAssetData(const FAssetData* InAssetData, const TArray& ArrayToSearch) +{ + check(InAssetData != nullptr); + static const FName NAME_Parent = TEXT("Parent"); + FString ParentPath = InAssetData->GetTagValueRef(NAME_Parent); + + int32 FirstCut = INDEX_NONE; + ParentPath.FindChar(L'\'', FirstCut); + + FName ParentPathName = NAME_None; + + if(FirstCut != INDEX_NONE) + { + ParentPathName = FName(*ParentPath.Mid(FirstCut + 1, ParentPath.Len() - FirstCut - 2)); + } + else + { + ParentPathName = FName(*ParentPath); + } + + if(ParentPathName.IsValid() && !ParentPathName.IsNone()) + { + return ArrayToSearch.FindByPredicate( + [&](FAssetData& Entry) + { + return Entry.ObjectPath == ParentPathName; + } + ); + } + + return nullptr; +} + +void SMaterialAnalyzer::Construct(const FArguments& InArgs, const TSharedRef& ConstructUnderMajorTab, const TSharedPtr& ConstructUnderWindow) +{ + TArray AllowedClasses; + AllowedClasses.Add(UMaterialInterface::StaticClass()); + + TSharedRef AssetPickerWidget = SNew(SObjectPropertyEntryBox) + .ObjectPath(this, &SMaterialAnalyzer::GetCurrentAssetPath) + .AllowedClass(UMaterialInterface::StaticClass()) + .OnObjectChanged(this, &SMaterialAnalyzer::OnAssetSelected) + .AllowClear(false) + .DisplayUseSelected(true) + .DisplayBrowse(true) + .NewAssetFactories(TArray()) + .IsEnabled(this, &SMaterialAnalyzer::IsMaterialSelectionAllowed); + + this->ChildSlot + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) + .BorderBackgroundColor(FLinearColor::Gray) // Darken the outer border + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(FMargin(5.0f, 5.0f, 5.0f, 5.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("MaterialToAnalyze", "Material To Analyze: ")) + ] + + SHorizontalBox::Slot() + .FillWidth(0.5f) + [ + AssetPickerWidget + ] + +SHorizontalBox::Slot() + .FillWidth(0.5f) + [ + SNullWidget::NullWidget + ] + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + [ + SNew(SSplitter) + .Orientation(EOrientation::Orient_Vertical) + + SSplitter::Slot() + [ + SNew(SBorder) + .Padding(0) + .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) + [ + SAssignNew(MaterialTree, SAnalyzedMaterialTree) + .ItemHeight(24.0f) + .TreeItemsSource(&MaterialTreeRoot) + .OnGenerateRow(this, &SMaterialAnalyzer::HandleReflectorTreeGenerateRow) + .OnGetChildren(this, &SMaterialAnalyzer::HandleReflectorTreeGetChildren) + .HeaderRow + ( + SNew(SHeaderRow) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_MaterialName) + .DefaultLabel(LOCTEXT("MaterialName", "Material Name")) + .FillWidth(0.80f) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_NumberOfChildren) + .DefaultLabel(LOCTEXT("NumberOfMaterialChildren", "Number of Children (Direct/Total)")) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_BasePropertyOverrides) + .DefaultLabel(LOCTEXT("BasePropertyOverrides", "Base Property Overrides")) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_MaterialLayerParameters) + .DefaultLabel(LOCTEXT("MaterialLayerParameters", "Material Layer Parameters")) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_StaticSwitchParameters) + .DefaultLabel(LOCTEXT("StaticSwitchParameters", "Static Switch Parameters")) + + SHeaderRow::Column(SAnalyzedMaterialNodeWidgetItem::NAME_StaticComponentMaskParameters) + .DefaultLabel(LOCTEXT("StaticComponenetMaskParameters", "Static Component Mask Parameters")) + ) + ] + ] + + SSplitter::Slot() + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) + .BorderBackgroundColor(FLinearColor::Gray) // Darken the outer border + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("Suggestions", "Suggestions")) + ] + + SVerticalBox::Slot() + [ + SAssignNew(SuggestionsBox, SScrollBox) + ] + ] + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(EVerticalAlignment::VAlign_Bottom) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .VAlign(VAlign_Center) + [ + SAssignNew(StatusBox, STextBlock) + .Text(LOCTEXT("Done", "Done")) + ] + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + .Padding(0,0,4,0) + [ + SAssignNew(StatusThrobber, SThrobber) + .Animate(SThrobber::EAnimation::None) + ] + ] + ] + ]; + + // Load the asset registry module to listen for updates + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + if(AssetRegistryModule.Get().IsLoadingAssets()) + { + StartAsyncWork(LOCTEXT("WaitingForAssetRegistry", "Waiting for Asset Registry to finish loading")); + bWaitingForAssetRegistryLoad = true; + } + else + { + SetupAssetRegistryCallbacks(); + BuildBasicMaterialTree(); + } +} + +void SMaterialAnalyzer::SetupAssetRegistryCallbacks() +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().OnAssetAdded().AddSP(this, &SMaterialAnalyzer::OnAssetAdded); +} + +void SMaterialAnalyzer::OnAssetAdded(const FAssetData& InAssetData) +{ + if(InAssetData.GetClass()->IsChildOf()) + { + RecentlyAddedAssetData.Add(InAssetData); + } +} + +void SMaterialAnalyzer::OnAssetSelected(const FAssetData& AssetData) +{ + if(AnalyzeTreeTask == nullptr) + { + CurrentlySelectedAsset = AssetData; + + const FAssetData* ParentAssetData = &AssetData; + const FAssetData* NextParentAssetData = FindParentAssetData(&AssetData, AssetDataArray); + // get the topmost parent + while (NextParentAssetData != nullptr) + { + ParentAssetData = NextParentAssetData; + NextParentAssetData = FindParentAssetData(ParentAssetData, AssetDataArray); + } + + // empty the previous tree root + MaterialTreeRoot.Empty(1); + // Add the new tree root + FAnalyzedMaterialNodeRef* NewRoot = AllMaterialTreeRoots.FindByPredicate([&](FAnalyzedMaterialNodeRef& Entry) + { + return Entry->ObjectPath == ParentAssetData->ObjectPath; + }); + check(NewRoot != nullptr); + + MaterialTreeRoot.Add(*NewRoot); + + MaterialTree->RequestTreeRefresh(); + + SuggestionsBox->ClearChildren(); + + AnalyzeTreeTask = new FAsyncTask(*NewRoot, AssetDataArray); + + StartAsyncWork(FText::Format(LOCTEXT("AnalyzingMaterial", "Analyzing {0}"), FText::FromString(AnalyzeTreeTask->GetTask().CurrentMaterialNode->Path))); + AnalyzeTreeTask->StartBackgroundTask(); + } +} + +void SMaterialAnalyzer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + + if (bWaitingForAssetRegistryLoad) + { + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + if(!AssetRegistryModule.Get().IsLoadingAssets()) + { + SetupAssetRegistryCallbacks(); + BuildBasicMaterialTree(); + bWaitingForAssetRegistryLoad = false; + } + } + else + { + if (BuildBaseMaterialTreeTask != nullptr && BuildBaseMaterialTreeTask->IsDone()) + { + delete BuildBaseMaterialTreeTask; + BuildBaseMaterialTreeTask = nullptr; + AsyncWorkFinished(FText::Format(FTextFormat(LOCTEXT("MaterialAnalyzer_DoneWithMaterialInterfaces", "Done with {0} MaterialInterfaces")), GetTotalNumberOfMaterialNodes())); + + } + + if (BuildBaseMaterialTreeTask == nullptr && RecentlyAddedAssetData.Num() > 0) + { + // Need to make this append to the previously generated list instead of erase all of the old info + // Current problem is that if we only have a portion of the asset registry it will create duplicate + // nodes since it won't find all parents in the tree. Need to modify the async task to not create + // nodes that don't have parents until we can find their parent. + AssetDataArray.Append(RecentlyAddedAssetData); + RecentlyAddedAssetData.Empty(); + AllMaterialTreeRoots.Empty(AllMaterialTreeRoots.Num()); + + BuildBaseMaterialTreeTask = new FAsyncTask(AllMaterialTreeRoots, AssetDataArray); + BuildBaseMaterialTreeTask->StartBackgroundTask(); + + StartAsyncWork(LOCTEXT("BuildingBasicTree", "Building Basic MaterialTree")); + } + + if (AnalyzeTreeTask != nullptr && AnalyzeTreeTask->IsDone()) + { + if (AnalyzeTreeTask->GetTask().LoadNextMaterial()) + { + StartAsyncWork(FText::Format(LOCTEXT("AnalyzingMaterial", "Analyzing {0}"), FText::FromString(AnalyzeTreeTask->GetTask().CurrentMaterialNode->Path))); + AnalyzeTreeTask->StartBackgroundTask(); + } + else + { + MaterialTree->RequestListRefresh(); + + // Kick off a check for identical permutations + // @todo make this a series of tests that users can choose to run + AnalyzeForIdenticalPermutationsTask = new FAsyncTask(AnalyzeTreeTask->GetTask().MaterialTreeRoot); + AnalyzeForIdenticalPermutationsTask->StartBackgroundTask(); + + delete AnalyzeTreeTask; + AnalyzeTreeTask = nullptr; + + StartAsyncWork(LOCTEXT("AnalyzingTreeForIdenticalPermutations", "Analyzing material tree for identical permutations")); + } + } + + if (AnalyzeForIdenticalPermutationsTask != nullptr && AnalyzeForIdenticalPermutationsTask->IsDone()) + { + MaterialTree->RequestListRefresh(); + AsyncWorkFinished(LOCTEXT("Done", "Done!")); + + TMultiMap Suggestions = AnalyzeForIdenticalPermutationsTask->GetTask().GetSuggestions(); + + Suggestions.KeySort([](int32 A, int32 B) { + return A > B; // sort to show most improvement possibility first + }); + + + + + int32 BackgroundColorCounter = 0; + for (auto It = Suggestions.CreateConstIterator(); It; ++It) + { + TSharedPtr SuggestionHeader = MakeShareable(new FPermutationSuggestionView()); + SuggestionHeader->Header = It.Value().Header; + for (FString Material : It.Value().Materials) + { + TSharedPtr SuggestionChild = MakeShareable(new FPermutationSuggestionView()); + SuggestionChild->Header = FText::FromString(Material); + SuggestionHeader->Children.Add(SuggestionChild); + } + SuggestionDataArray.Add(SuggestionHeader); + } + + SuggestionsBox->AddSlot() + [ + SAssignNew(SuggestionsTree, STreeView>) + .TreeItemsSource(&SuggestionDataArray) + .OnGenerateRow(this, &SMaterialAnalyzer::OnGenerateSuggestionRow) + .OnGetChildren(this, &SMaterialAnalyzer::OnGetSuggestionChildren) + ]; + + + + + delete AnalyzeForIdenticalPermutationsTask; + AnalyzeForIdenticalPermutationsTask = nullptr; + } + } +} + +TSharedRef< ITableRow > SMaterialAnalyzer::OnGenerateSuggestionRow(TSharedPtr Item, const TSharedRef< STableViewBase >& OwnerTable) +{ + if (Item->Children.Num() > 0) + { + return SNew(STableRow>, OwnerTable) + [ + SNew(SVerticalBox) + +SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Bottom) + [ + SNew(SEditableText) + .IsReadOnly(true) + .Text(Item->Header) + ] + + SVerticalBox::Slot() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .Visibility(this, &SMaterialAnalyzer::ShouldShowAdvancedRecommendations, Item) + .Text(LOCTEXT("PermutationRecommendation", "It is recommended that you reparent them in a way so only dynamic parameters differ.")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + +SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .ButtonStyle(FEditorStyle::Get(), "SimpleSharpButton") + .Visibility(this, &SMaterialAnalyzer::ShouldShowAdvancedRecommendations, Item) + .OnClicked(this, &SMaterialAnalyzer::CreateLocalSuggestionCollection, Item) + .ContentPadding(FMargin(2.0f)) + .Content() + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush("ContentBrowser.AddCollectionButtonIcon")) + .ColorAndOpacity(FSlateColor::UseForeground()) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .Padding(2.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateLocalCollection", "Create Local Collection")) + ] + ] + ] + ] + ]; + } + return SNew(STableRow>, OwnerTable) + [ + SNew(SEditableText) + .IsReadOnly(true) + .Text(Item->Header) + ]; +} + +EVisibility SMaterialAnalyzer::ShouldShowAdvancedRecommendations(TSharedPtr Item) const +{ + return SuggestionsTree->IsItemExpanded(Item) ? EVisibility::Visible : EVisibility::Collapsed; +} + +void SMaterialAnalyzer::OnGetSuggestionChildren(TSharedPtr InParent, TArray< TSharedPtr >& OutChildren) +{ + OutChildren = InParent->Children; +} + +FReply SMaterialAnalyzer::CreateLocalSuggestionCollection(TSharedPtr InSuggestion) +{ + TArray AllSelectedPackageNames; + ECollectionShareType::Type ShareType = ECollectionShareType::CST_Local; + for (TSharedPtr Child : InSuggestion->Children) + { + AllSelectedPackageNames.Add(Child->Header.ToString()); + } + + if (AllSelectedPackageNames.Num() > 0) + { + FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); + + FString FirstAssetString = CurrentlySelectedAsset.AssetName.ToString() + TEXT("_") + FString::FromInt(InSuggestion->Children.Num()); + FName FirstAssetName = FName(*FirstAssetString); + + CollectionManagerModule.Get().CreateUniqueCollectionName(FirstAssetName, ShareType, FirstAssetName); + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + TArray PackageNamesToAddToCollection; + + TArray PackageNameSet; + for (FString PackageToAdd : AllSelectedPackageNames) + { + PackageNameSet.Add(FName(*FPaths::GetBaseFilename(*PackageToAdd, false))); + } + + IAssetManagerEditorModule::Get().WriteCollection(FirstAssetName, ShareType, PackageNameSet, true); + } + return FReply::Handled(); +} + +void SMaterialAnalyzer::StartAsyncWork(const FText& WorkText) +{ + if(StatusBox.IsValid()) + { + StatusBox->SetText(WorkText); + } + + if(StatusThrobber.IsValid()) + { + StatusThrobber->SetAnimate(SThrobber::Horizontal); + StatusThrobber->SetVisibility(EVisibility::SelfHitTestInvisible); + } + + bAllowMaterialSelection = false; +} + +void SMaterialAnalyzer::AsyncWorkFinished(const FText& CompleteText) +{ + if (StatusBox.IsValid()) + { + StatusBox->SetText(CompleteText); + } + + if (StatusThrobber.IsValid()) + { + StatusThrobber->SetAnimate(SThrobber::None); + StatusThrobber->SetVisibility(EVisibility::Collapsed); + } + + bAllowMaterialSelection = true; +} + +int32 SMaterialAnalyzer::GetTotalNumberOfMaterialNodes() +{ + int32 NumMaterialNodes = AllMaterialTreeRoots.Num(); + + for(int i = 0; i < AllMaterialTreeRoots.Num(); ++i) + { + NumMaterialNodes += AllMaterialTreeRoots[i]->TotalNumberOfChildren(); + } + + return NumMaterialNodes; +} + +FString SMaterialAnalyzer::GetCurrentAssetPath() const +{ + return CurrentlySelectedAsset.IsValid() ? CurrentlySelectedAsset.ObjectPath.ToString() : FString(""); +} + +void SMaterialAnalyzer::BuildBasicMaterialTree() +{ + static const FName AssetRegistryName(TEXT("AssetRegistry")); + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryName); + + IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); + + AssetRegistry.GetAssetsByClass(UMaterial::StaticClass()->GetFName(), AssetDataArray, true); + AssetRegistry.GetAssetsByClass(UMaterialInstance::StaticClass()->GetFName(), AssetDataArray, true); + + if (BuildBaseMaterialTreeTask == nullptr && AssetDataArray.Num() > 0) + { + AllMaterialTreeRoots.Empty(AllMaterialTreeRoots.Num()); + BuildBaseMaterialTreeTask = new FAsyncTask(AllMaterialTreeRoots, AssetDataArray); + BuildBaseMaterialTreeTask->StartBackgroundTask(); + + StartAsyncWork(LOCTEXT("BuildingBasicTree", "Building basic material tree")); + + if (StatusThrobber.IsValid()) + { + StatusThrobber->SetAnimate(SThrobber::EAnimation::Horizontal); + } + } +} + +TSharedRef SMaterialAnalyzer::HandleReflectorTreeGenerateRow(FAnalyzedMaterialNodeRef InMaterialNode, const TSharedRef& OwnerTable) +{ + TSharedPtr NewWidget = SNew(SAnalyzedMaterialNodeWidgetItem, OwnerTable) + .MaterialInfoToVisualize(InMaterialNode); + + // if we're the base level we're going to expand right away + if(!InMaterialNode->Parent.IsValid()) + { + MaterialTree->SetItemExpansion(InMaterialNode, true); + } + + return NewWidget.ToSharedRef(); +} + +void SMaterialAnalyzer::HandleReflectorTreeGetChildren(FAnalyzedMaterialNodeRef InMaterialNode, TArray& OutChildren) +{ + OutChildren = InMaterialNode->GetChildNodes(); +} + +FAnalyzedMaterialNodePtr FBuildBasicMaterialTreeAsyncTask::FindOrMakeBranchNode(FAnalyzedMaterialNodePtr ParentNode, const FAssetData* ChildData) +{ + check(ChildData != nullptr); + FAnalyzedMaterialNodeRef* OutNode = nullptr; + + FName ChildName = ChildData->ObjectPath; + + TArray& NodesToSearch = ParentNode.IsValid() ? ParentNode->GetChildNodes() : MaterialTreeRoot; + + OutNode = NodesToSearch.FindByPredicate([&](FAnalyzedMaterialNodeRef& Entry) { return Entry->ObjectPath == ChildName; }); + + if (OutNode == nullptr) + { + FAnalyzedMaterialNode ChildNode; + ChildNode.Path = ChildData->AssetName.ToString(); + ChildNode.ObjectPath = ChildData->ObjectPath; + ChildNode.Parent = ParentNode; + NodesToSearch.Add(FAnalyzedMaterialNodeRef(new FAnalyzedMaterialNode(ChildNode))); + OutNode = &NodesToSearch[NodesToSearch.Num() - 1]; + } + + return FAnalyzedMaterialNodePtr (*OutNode); +} + +void FBuildBasicMaterialTreeAsyncTask::DoWork() +{ + for(int i = 0; i < AssetDataToAnalyze.Num(); ++i) + { + const FAssetData& AssetData = AssetDataToAnalyze[i]; + + TArray FullBranch; + + const FAssetData* CurrentBranchNode = &AssetData; + while(CurrentBranchNode) + { + FullBranch.Add(CurrentBranchNode); + CurrentBranchNode = FindParentAssetData(CurrentBranchNode, AssetDataToAnalyze); + } + + FAnalyzedMaterialNodePtr ParentNode = nullptr; + + for(int Depth = FullBranch.Num() - 1; Depth >= 0; --Depth) + { + ParentNode = FindOrMakeBranchNode(ParentNode, FullBranch[Depth]); + } + } +} + +bool FAnalyzeMaterialTreeAsyncTask::LoadNextMaterial() +{ + if (CurrentMaterialQueueIndex < MaterialQueue.Num()) + { + CurrentMaterialNode = MaterialQueue[CurrentMaterialQueueIndex]; + check(CurrentMaterialNode->ObjectPath.IsValid()); + + CurrentMaterialInterface = FindObject(NULL, *CurrentMaterialNode->ObjectPath.ToString()); + if (CurrentMaterialInterface == nullptr) + { + CurrentMaterialInterface = LoadObject(NULL, *CurrentMaterialNode->ObjectPath.ToString()); + check(CurrentMaterialInterface); + } + + return true; + } + + return false; +} + +void FAnalyzeMaterialTreeAsyncTask::DoWork() +{ + MaterialQueue.Append(CurrentMaterialNode->GetChildNodes()); + + TArray MaterialLayersParameterInfo; + + check(CurrentMaterialInterface != nullptr); + + TArray ParameterInfo; + TArray Guids; + + UMaterial* CurrentMaterial = Cast(CurrentMaterialInterface); + + bool bCanBeOverriden = CurrentMaterial != nullptr; + + if(CurrentMaterial != nullptr) + { + CurrentMaterial->GetAllMaterialLayersParameterInfo(MaterialLayerParameterInfo, MaterialLayerGuids); + + CurrentMaterial->GetAllStaticSwitchParameterInfo(StaticSwitchParameterInfo, StaticSwitchGuids); + + CurrentMaterial->GetAllStaticComponentMaskParameterInfo(StaticMaskParameterInfo, StaticMaskGuids); + } + + CurrentMaterialNode->BasePropertyOverrides.Empty(BasePropertyOverrideNames.Num()); + + for (const TPair& BasePropertyOverrideName : BasePropertyOverrideNames) + { + float TempValue = 0.0f; + bool bIsOverridden = false; + + UMaterialInstance* CurrentMaterialInstance = Cast(CurrentMaterialInterface); + + if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_OpacityMaskClipValue"))) + { + TempValue = CurrentMaterialInterface->GetOpacityMaskClipValue(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_OpacityMaskClipValue; + } + } + else if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_BlendMode"))) + { + TempValue = (float)CurrentMaterialInterface->GetBlendMode(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_BlendMode; + } + } + else if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_ShadingModel"))) + { + TempValue = (float)CurrentMaterialInterface->GetShadingModel(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_ShadingModel; + } + } + else if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_DitheredLODTransition"))) + { + TempValue = (float)CurrentMaterialInterface->IsDitheredLODTransition(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_DitheredLODTransition; + } + } + else if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_CastDynamicShadowAsMasked"))) + { + TempValue = CurrentMaterialInterface->GetCastShadowAsMasked(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_CastDynamicShadowAsMasked; + } + } + else if (BasePropertyOverrideName.Key.IsEqual(TEXT("bOverride_TwoSided"))) + { + TempValue = CurrentMaterialInterface->IsTwoSided(); + if (CurrentMaterialInstance) + { + bIsOverridden = CurrentMaterialInstance->BasePropertyOverrides.bOverride_TwoSided; + } + } + + // Check the parent for this variable + FAnalyzedMaterialNodePtr Parent = CurrentMaterialNode->Parent; + if (!bIsOverridden && Parent.IsValid()) + { + // We shouldn't be able to get in here for the base Material + FBasePropertyOverrideNodeRef ParentParameter = Parent->FindBasePropertyOverride(BasePropertyOverrideName.Value); + + CurrentMaterialNode->BasePropertyOverrides.Add(FBasePropertyOverrideNodeRef( + new FBasePropertyOverrideNode(ParentParameter->ParameterName, + ParentParameter->ParameterID, + ParentParameter->ParameterValue, + false))); + } + else + { + CurrentMaterialNode->BasePropertyOverrides.Add(FBasePropertyOverrideNodeRef( + new FBasePropertyOverrideNode(BasePropertyOverrideName.Value, BasePropertyOverrideName.Key, TempValue, bIsOverridden))); + } + } + + + CurrentMaterialNode->MaterialLayerParameters.Empty(MaterialLayerParameterInfo.Num()); + + for (int ParameterIndex = 0; ParameterIndex < MaterialLayerParameterInfo.Num(); ++ParameterIndex) + { + FMaterialLayersFunctions Functions; + bool bIsOverridden = CurrentMaterialInterface->GetMaterialLayersParameterValue(MaterialLayerParameterInfo[ParameterIndex], Functions, MaterialLayerGuids[ParameterIndex], false); + + if (!bIsOverridden) + { + // Check the parent for this variable + FAnalyzedMaterialNodePtr Parent = CurrentMaterialNode->Parent; + // We shouldn't be able to get in here for the base Material + check(Parent.IsValid()); + + FStaticMaterialLayerParameterNodeRef ParentParameter = Parent->FindMaterialLayerParameter(MaterialLayerParameterInfo[ParameterIndex].Name); + + CurrentMaterialNode->MaterialLayerParameters.Add(FStaticMaterialLayerParameterNodeRef( + new FStaticMaterialLayerParameterNode(ParentParameter->ParameterName, + ParentParameter->ParameterValue, + false))); + } + else + { + CurrentMaterialNode->MaterialLayerParameters.Add(FStaticMaterialLayerParameterNodeRef( + new FStaticMaterialLayerParameterNode(MaterialLayerParameterInfo[ParameterIndex].Name, + Functions.GetStaticPermutationString(), + true))); + } + } + + CurrentMaterialNode->StaticSwitchParameters.Empty(StaticSwitchParameterInfo.Num()); + + for (int ParameterIndex = 0; ParameterIndex < StaticSwitchParameterInfo.Num(); ++ParameterIndex) + { + bool bStaticSwitchValue; + bool bIsOverridden = CurrentMaterialInterface->GetStaticSwitchParameterValue(StaticSwitchParameterInfo[ParameterIndex], bStaticSwitchValue, StaticSwitchGuids[ParameterIndex], false, false); + + if (!bIsOverridden) + { + // Check the parent for this variable + FAnalyzedMaterialNodePtr Parent = CurrentMaterialNode->Parent; + // We shouldn't be able to get in here for the base Material + check(Parent.IsValid()); + + FStaticSwitchParameterNodeRef ParentParameter = Parent->FindStaticSwitchParameter(StaticSwitchParameterInfo[ParameterIndex].Name); + + CurrentMaterialNode->StaticSwitchParameters.Add(FStaticSwitchParameterNodeRef( + new FStaticSwitchParameterNode(ParentParameter->ParameterName, + ParentParameter->ParameterValue, + false))); + } + else + { + CurrentMaterialNode->StaticSwitchParameters.Add(FStaticSwitchParameterNodeRef( + new FStaticSwitchParameterNode(StaticSwitchParameterInfo[ParameterIndex].Name, bStaticSwitchValue, true))); + } + } + + CurrentMaterialNode->StaticComponentMaskParameters.Empty(StaticMaskParameterInfo.Num()); + + for (int ParameterIndex = 0; ParameterIndex < StaticMaskParameterInfo.Num(); ++ParameterIndex) + { + bool R, G, B, A; + + bool bIsOverridden = CurrentMaterialInterface->GetStaticComponentMaskParameterValue(StaticMaskParameterInfo[ParameterIndex], R, G, B, A, StaticMaskGuids[ParameterIndex], false, false); + + if(!bIsOverridden) + { + // Check the parent for this variable + FAnalyzedMaterialNodePtr Parent = CurrentMaterialNode->Parent; + // We shouldn't be able to get in here for the base Material + check(Parent.IsValid()); + + FStaticComponentMaskParameterNodeRef ParentParameter = Parent->FindStaticComponentMaskParameter(StaticMaskParameterInfo[ParameterIndex].Name); + + CurrentMaterialNode->StaticComponentMaskParameters.Add(FStaticComponentMaskParameterNodeRef( + new FStaticComponentMaskParameterNode(ParentParameter->ParameterName, + ParentParameter->R, + ParentParameter->G, + ParentParameter->B, + ParentParameter->A, + false))); + } + else + { + CurrentMaterialNode->StaticComponentMaskParameters.Add(FStaticComponentMaskParameterNodeRef( + new FStaticComponentMaskParameterNode(StaticMaskParameterInfo[ParameterIndex].Name, R, G, B, A, true))); + } + } + + CurrentMaterialQueueIndex++; +} + +bool FAnalyzeForIdenticalPermutationsAsyncTask::CreateMaterialPermutationHashForNode(const FAnalyzedMaterialNodeRef& MaterialNode, uint32& OutHash) +{ + TArray ByteArray; + + bool bAnyOverrides = false; + + for (int ParameterIndex = 0; ParameterIndex < MaterialNode->BasePropertyOverrides.Num(); ++ParameterIndex) + { + FString FloatToHash = FString::SanitizeFloat(MaterialNode->BasePropertyOverrides[ParameterIndex]->ParameterValue); + ByteArray.Append(TCHAR_TO_ANSI(*FloatToHash), FloatToHash.Len()); + bAnyOverrides = bAnyOverrides || MaterialNode->BasePropertyOverrides[ParameterIndex]->bOverride; + } + + for (int ParameterIndex = 0; ParameterIndex < MaterialNode->MaterialLayerParameters.Num(); ++ParameterIndex) + { + ByteArray.Append(TCHAR_TO_ANSI(*MaterialNode->MaterialLayerParameters[ParameterIndex]->ParameterValue), MaterialNode->MaterialLayerParameters[ParameterIndex]->ParameterValue.Len()); + bAnyOverrides = bAnyOverrides || MaterialNode->MaterialLayerParameters[ParameterIndex]->bOverride; + } + + for(int ParameterIndex = 0; ParameterIndex < MaterialNode->StaticSwitchParameters.Num(); ++ParameterIndex) + { + ByteArray.Add(MaterialNode->StaticSwitchParameters[ParameterIndex]->ParameterValue ? 1 : 0); + bAnyOverrides = bAnyOverrides || MaterialNode->StaticSwitchParameters[ParameterIndex]->bOverride; + } + + for(int ParameterIndex = 0; ParameterIndex < MaterialNode->StaticComponentMaskParameters.Num(); ++ParameterIndex) + { + FStaticComponentMaskParameterNodeRef NodeRef = MaterialNode->StaticComponentMaskParameters[ParameterIndex]; + ByteArray.Add(NodeRef->R ? 1 : 0); + ByteArray.Add(NodeRef->G ? 1 : 0); + ByteArray.Add(NodeRef->B ? 1 : 0); + ByteArray.Add(NodeRef->A ? 1 : 0); + bAnyOverrides = bAnyOverrides || NodeRef->bOverride; + } + + OutHash = CityHash32(ByteArray.GetData(), ByteArray.Num()); + + return bAnyOverrides; +} + +void FAnalyzeForIdenticalPermutationsAsyncTask::DoWork() +{ + for(int i = 0; i < MaterialQueue.Num(); ++i) + { + FAnalyzedMaterialNodeRef CurrentMaterialNode = MaterialQueue[i]; + + MaterialQueue.Append(CurrentMaterialNode->GetChildNodes()); + + uint32 MaterialPermutationHash = 0; + + if(CreateMaterialPermutationHashForNode(CurrentMaterialNode, MaterialPermutationHash)) + { + MaterialPermutationHashToMaterialObjectPath.FindOrAdd(MaterialPermutationHash).Add(CurrentMaterialNode->ObjectPath); + } + } + + GatherSuggestions(); +} + +void FAnalyzeForIdenticalPermutationsAsyncTask::GatherSuggestions() +{ + for (TPair>& IdenticalPermutations : MaterialPermutationHashToMaterialObjectPath) + { + if (IdenticalPermutations.Value.Num() > 1) + { + TArray AllNames; + AssetCount = IdenticalPermutations.Value.Num(); + for (int i = 0; i < IdenticalPermutations.Value.Num(); ++i) + { + FString PermutationString = IdenticalPermutations.Value[i].ToString(); + AllNames.Add(PermutationString); + } + + FPermutationSuggestionData NewData = FPermutationSuggestionData(FText::Format(LOCTEXT("IdenticalPermutationSuggestions", + "The following {0} materials all have identical permutations."), + FText::AsNumber(AssetCount)), + AllNames); + + Suggestions.Add + ( + AssetCount, + NewData + ); + } + } +} + + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.h b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.h new file mode 100644 index 000000000000..4fb8b859162c --- /dev/null +++ b/Engine/Plugins/Editor/MaterialAnalyzer/Source/Private/SMaterialAnalyzer.h @@ -0,0 +1,283 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Queue.h" +#include "Input/Reply.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Engine/GameViewportClient.h" +#include "Widgets/SCompoundWidget.h" +#include "Framework/Commands/UICommandList.h" +#include "Framework/Docking/TabManager.h" +#include "Widgets/Views/STreeView.h" +#include "AnalyzedMaterialNode.h" +#include "SAnalyzedMaterialNodeWidgetItem.h" +#include "Materials/Material.h" +#include "Materials/MaterialInstance.h" +#include "ContentBrowserModule.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Images/SThrobber.h" + +struct FBuildBasicMaterialTreeAsyncTask : FNonAbandonableTask +{ +public: + /** File data loaded for the async read */ + TArray& MaterialTreeRoot; + + TArray AssetDataToAnalyze; + + /** Initializes the variables needed to load and verify the data */ + FBuildBasicMaterialTreeAsyncTask(TArray& InMaterialTreeRoot, const TArray& InAssetDataToAnalyze): + MaterialTreeRoot(InMaterialTreeRoot), + AssetDataToAnalyze(InAssetDataToAnalyze) + { + + } + + FAnalyzedMaterialNodePtr FindOrMakeBranchNode(FAnalyzedMaterialNodePtr ParentNode, const FAssetData* ChildData); + + /** + * Loads and hashes the file data. Empties the data if the hash check fails + */ + void DoWork(); + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FBuildBasicMaterialTreeAsyncTask, STATGROUP_ThreadPoolAsyncTasks); + } +}; + +struct FAnalyzeMaterialTreeAsyncTask +{ +public: + FAnalyzedMaterialNodeRef MaterialTreeRoot; + const TArray& AssetDataToAnalyze; + + TArray MaterialQueue; + + int32 CurrentMaterialQueueIndex; + + FAnalyzedMaterialNodeRef CurrentMaterialNode; + UMaterialInterface* CurrentMaterialInterface; + + TArray BasePropertyOverrideInfo; + + TArray MaterialLayerParameterInfo; + TArray MaterialLayerGuids; + + TArray StaticSwitchParameterInfo; + TArray StaticSwitchGuids; + + TArray StaticMaskParameterInfo; + TArray StaticMaskGuids; + + + FAnalyzeMaterialTreeAsyncTask(FAnalyzedMaterialNodeRef InMaterialTreeRoot, const TArray& InAssetDataToAnalyze): + MaterialTreeRoot(InMaterialTreeRoot), + AssetDataToAnalyze(InAssetDataToAnalyze), + CurrentMaterialQueueIndex(0), + CurrentMaterialNode(InMaterialTreeRoot) + { + MaterialQueue.Empty(MaterialTreeRoot->TotalNumberOfChildren()); + MaterialQueue.Add(MaterialTreeRoot); + LoadNextMaterial(); + } + + bool LoadNextMaterial(); + + void DoWork(); + + // need to make abandon-able eventually + bool CanAbandon() + { + return false; + } + void Abandon() + { + } + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FAnalyzeMaterialTreeAsyncTask, STATGROUP_ThreadPoolAsyncTasks); + } +}; + +struct FPermutationSuggestionData +{ + FText Header; + TArray Materials; + + FPermutationSuggestionData(const FText& InHeader, TArray InMaterials) + : Header(InHeader) + , Materials(InMaterials) + { + } + +}; + +struct FPermutationSuggestionView +{ + FText Header; + TArray> Children; +}; + +struct FAnalyzeForSuggestions +{ + +public: + TMultiMap GetSuggestions() + { + return Suggestions; + } + +protected: + FAnalyzeForSuggestions() + :Suggestions(TMultiMap()) + { + } + + virtual ~FAnalyzeForSuggestions() + { + + } + + TMultiMap Suggestions; + + virtual void GatherSuggestions() = 0; + +}; + +struct FAnalyzeForIdenticalPermutationsAsyncTask : FAnalyzeForSuggestions +{ +public: + virtual ~FAnalyzeForIdenticalPermutationsAsyncTask() + { + + } + + FAnalyzedMaterialNodeRef MaterialTreeRoot; + + TArray MaterialQueue; + + TMap> MaterialPermutationHashToMaterialObjectPath; + + int32 AssetCount; + + FAnalyzeForIdenticalPermutationsAsyncTask(FAnalyzedMaterialNodeRef InMaterialTreeRoot) : + MaterialTreeRoot(InMaterialTreeRoot) + { + MaterialQueue.Empty(MaterialTreeRoot->TotalNumberOfChildren()); + MaterialQueue.Add(MaterialTreeRoot); + } + + bool CreateMaterialPermutationHashForNode(const FAnalyzedMaterialNodeRef& MaterialNode, uint32& OutHash); + void DoWork(); + + // need to make abandon-able eventually + bool CanAbandon() + { + return false; + } + void Abandon() + { + } + + virtual void GatherSuggestions() override; + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FAnalyzeForIdenticalPermutationsAsyncTask, STATGROUP_ThreadPoolAsyncTasks); + } +}; + +class SMaterialAnalyzer : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SMaterialAnalyzer) + { } + SLATE_END_ARGS() + + typedef STreeView SAnalyzedMaterialTree; + +public: + SMaterialAnalyzer(); + virtual ~SMaterialAnalyzer(); + + /** + * Constructs the application. + * + * @param InArgs The Slate argument list. + * @param ConstructUnderMajorTab The major tab which will contain the session front-end. + * @param ConstructUnderWindow The window in which this widget is being constructed. + * @param InStyleSet The style set to use. + */ + void Construct(const FArguments& InArgs, const TSharedRef& ConstructUnderMajorTab, const TSharedPtr& ConstructUnderWindow); + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + + TSharedRef< ITableRow > OnGenerateSuggestionRow(TSharedPtr Item, const TSharedRef< STableViewBase >& OwnerTable); + EVisibility ShouldShowAdvancedRecommendations(TSharedPtr Item) const; + void OnAssetAdded(const FAssetData& InAssetData); + +protected: + + TArray AssetDataArray; + TArray RecentlyAddedAssetData; + TArray RecentlyRemovedAssetData; + + TSharedPtr MaterialTree; + TSharedPtr StatusBox; + TSharedPtr SuggestionsBox; + TSharedPtr StatusThrobber; + + TArray AllMaterialTreeRoots; + TArray MaterialTreeRoot; + + /** The tree view widget */ + TSharedPtr>> SuggestionsTree; + TArray> SuggestionDataArray; + + FAsyncTask* BuildBaseMaterialTreeTask; + FAsyncTask* AnalyzeTreeTask; + FAsyncTask* AnalyzeForIdenticalPermutationsTask; + + bool bRequestedTreeRefresh; + + FAssetData CurrentlySelectedAsset; + + bool bWaitingForAssetRegistryLoad; + + void OnAssetSelected(const FAssetData& AssetData); + + void BuildBasicMaterialTree(); + + int32 GetTotalNumberOfMaterialNodes(); + + void SetupAssetRegistryCallbacks(); + + void OnGetSuggestionChildren(TSharedPtr InParent, TArray< TSharedPtr >& OutChildren); + FReply CreateLocalSuggestionCollection(TSharedPtr InSuggestion); + void StartAsyncWork(const FText& WorkText); + + void AsyncWorkFinished(const FText& CompleteText); + + FString GetCurrentAssetPath() const; + +private: + + /** Callback for generating a row in the reflector tree view. */ + TSharedRef HandleReflectorTreeGenerateRow(FAnalyzedMaterialNodeRef InReflectorNode, const TSharedRef& OwnerTable); + + /** Callback for getting the child items of the given reflector tree node. */ + void HandleReflectorTreeGetChildren(FAnalyzedMaterialNodeRef InReflectorNode, TArray& OutChildren); + + bool IsMaterialSelectionAllowed() const + { + return bAllowMaterialSelection; + } + +private: + bool bAllowMaterialSelection; +}; \ No newline at end of file diff --git a/Engine/Plugins/Experimental/ControlRig/Config/BaseControlRig.ini b/Engine/Plugins/Experimental/ControlRig/Config/BaseControlRig.ini new file mode 100644 index 000000000000..06742b5a1b33 --- /dev/null +++ b/Engine/Plugins/Experimental/ControlRig/Config/BaseControlRig.ini @@ -0,0 +1,6 @@ +[CoreRedirects] ++ClassRedirects=(OldName="/Script/ControlRigEditor.ControlRigBlueprint", NewName="/Script/ControlRigDeveloper.ControlRigBlueprint") ++ClassRedirects=(OldName="/Script/ControlRigEditor.ControlRigGraph", NewName="/Script/ControlRigDeveloper.ControlRigGraph") ++ClassRedirects=(OldName="/Script/ControlRigEditor.ControlRigGraphNode", NewName="/Script/ControlRigDeveloper.ControlRigGraphNode") ++ClassRedirects=(OldName="/Script/ControlRigEditor.ControlRigGraphSchema", NewName="/Script/ControlRigDeveloper.ControlRigGraphSchema") ++StructRedirects=(OldName="/Script/ControlRigEditor.ControlRigBlueprintPropertyLink", NewName="/Script/ControlRigDeveloper.ControlRigBlueprintPropertyLink") diff --git a/Engine/Plugins/Experimental/ControlRig/ControlRig.uplugin b/Engine/Plugins/Experimental/ControlRig/ControlRig.uplugin index 1b0bbeb18331..a4a504b4fa6f 100644 --- a/Engine/Plugins/Experimental/ControlRig/ControlRig.uplugin +++ b/Engine/Plugins/Experimental/ControlRig/ControlRig.uplugin @@ -21,6 +21,11 @@ "Type" : "Runtime", "LoadingPhase" : "PreDefault" }, + { + "Name" : "ControlRigDeveloper", + "Type" : "Developer", + "LoadingPhase" : "PreDefault" + }, { "Name" : "ControlRigEditor", "Type" : "Editor", diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/ControlRig.Build.cs b/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/ControlRig.Build.cs index 249335691fc2..349c0efc2c92 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/ControlRig.Build.cs +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/ControlRig.Build.cs @@ -42,6 +42,9 @@ namespace UnrealBuildTool.Rules "PropertyEditor", } ); + + PrivateIncludePathModuleNames.Add("ControlRigEditor"); + DynamicallyLoadedModuleNames.Add("ControlRigEditor"); } } } diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/Private/ControlRig.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/Private/ControlRig.cpp index 89f6f674c60a..91cf9a0a4570 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/Private/ControlRig.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRig/Private/ControlRig.cpp @@ -47,6 +47,8 @@ UWorld* UControlRig::GetWorld() const void UControlRig::Initialize() { + QUICK_SCOPE_CYCLE_COUNTER(STAT_ControlRig_Initialize); + // initialize hierarchy refs // @todo re-think { diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/ControlRigDeveloper.Build.cs b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/ControlRigDeveloper.Build.cs new file mode 100644 index 000000000000..7087e63bf488 --- /dev/null +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/ControlRigDeveloper.Build.cs @@ -0,0 +1,47 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +namespace UnrealBuildTool.Rules +{ + public class ControlRigDeveloper : ModuleRules + { + public ControlRigDeveloper(ReadOnlyTargetRules Target) : base(Target) + { + PrivateIncludePaths.Add("ControlRigDeveloper/Private"); + + // Copying some these from ControlRig.build.cs, our deps are likely leaner + // and therefore these could be pruned if needed: + PrivateDependencyModuleNames.AddRange( + new string[] + { + "AnimGraphRuntime", + "ControlRig", + "Core", + "CoreUObject", + "Engine", + "KismetCompiler", + "MovieScene", + "MovieSceneTracks", + "PropertyPath", + "Slate", + "SlateCore", + "TimeManagement" + } + ); + + if (Target.bBuildEditor == true) + { + PrivateDependencyModuleNames.AddRange( + new string[] + { + "UnrealEd", + "BlueprintGraph", + "PropertyEditor", + } + ); + + PrivateIncludePathModuleNames.Add("ControlRigEditor"); + DynamicallyLoadedModuleNames.Add("ControlRigEditor"); + } + } + } +} diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprint.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprint.cpp new file mode 100644 index 000000000000..2f2d56a5dddc --- /dev/null +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprint.cpp @@ -0,0 +1,76 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "ControlRigBlueprint.h" +#include "ControlRigBlueprintGeneratedClass.h" +#include "EdGraph/EdGraph.h" +#include "Modules/ModuleManager.h" +#include "Engine/SkeletalMesh.h" +#include "BlueprintActionDatabaseRegistrar.h" + +#if WITH_EDITOR +#include "IControlRigEditorModule.h" +#endif//WITH_EDITOR + +#define LOCTEXT_NAMESPACE "ControlRigBlueprint" + +UControlRigBlueprint::UControlRigBlueprint() +{ +} + +UControlRigBlueprintGeneratedClass* UControlRigBlueprint::GetControlRigBlueprintGeneratedClass() const +{ + UControlRigBlueprintGeneratedClass* Result = Cast(*GeneratedClass); + return Result; +} + +UControlRigBlueprintGeneratedClass* UControlRigBlueprint::GetControlRigBlueprintSkeletonClass() const +{ + UControlRigBlueprintGeneratedClass* Result = Cast(*SkeletonGeneratedClass); + return Result; +} +UClass* UControlRigBlueprint::GetBlueprintClass() const +{ + return UControlRigBlueprintGeneratedClass::StaticClass(); +} + +void UControlRigBlueprint::LoadModulesRequiredForCompilation() +{ +} + +void UControlRigBlueprint::MakePropertyLink(const FString& InSourcePropertyPath, const FString& InDestPropertyPath) +{ + PropertyLinks.AddUnique(FControlRigBlueprintPropertyLink(InSourcePropertyPath, InDestPropertyPath)); +} + +USkeletalMesh* UControlRigBlueprint::GetPreviewMesh() const +{ + if (!PreviewSkeletalMesh.IsValid()) + { + PreviewSkeletalMesh.LoadSynchronous(); + } + + return PreviewSkeletalMesh.Get(); +} + +void UControlRigBlueprint::SetPreviewMesh(USkeletalMesh* PreviewMesh, bool bMarkAsDirty/*=true*/) +{ + if(bMarkAsDirty) + { + Modify(); + } + + PreviewSkeletalMesh = PreviewMesh; +} + +void UControlRigBlueprint::GetTypeActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + IControlRigEditorModule::Get().GetTypeActions(this, ActionRegistrar); +} + +void UControlRigBlueprint::GetInstanceActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + IControlRigEditorModule::Get().GetInstanceActions(this, ActionRegistrar); +} + +#undef LOCTEXT_NAMESPACE + diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintCompiler.cpp similarity index 99% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.cpp rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintCompiler.cpp index 79f8de2bf2e5..cfbd3f6b5980 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintCompiler.cpp @@ -5,8 +5,8 @@ #include "KismetCompiler.h" #include "ControlRigBlueprint.h" #include "Units/RigUnit.h" -#include "ControlRigGraph.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" #include "ControlRigBlueprintGeneratedClass.h" #include "Kismet2/KismetReinstanceUtilities.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintUtils.cpp similarity index 99% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.cpp rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintUtils.cpp index d64367cfa89c..c53e2b1515a6 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigBlueprintUtils.cpp @@ -5,7 +5,7 @@ #include "Units/RigUnit.h" #include "UObject/UObjectIterator.h" #include "ControlRig.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "ControlRigBlueprint.h" #define LOCTEXT_NAMESPACE "ControlRigBlueprintUtils" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigDeveloper.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigDeveloper.cpp new file mode 100644 index 000000000000..250388cd5084 --- /dev/null +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/ControlRigDeveloper.cpp @@ -0,0 +1,14 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "ControlRigDeveloper.h" +#include "Modules/ModuleManager.h" + +class FControlRigDeveloperModule : public IControlRigDeveloperModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override {} + virtual void ShutdownModule() override {} +}; + +IMPLEMENT_MODULE(FControlRigDeveloperModule, ControlRigDeveloper) diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraph.cpp similarity index 82% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.cpp rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraph.cpp index 96c6c254b609..71507027b416 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraph.cpp @@ -1,8 +1,8 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. -#include "ControlRigGraph.h" +#include "Graph/ControlRigGraph.h" #include "ControlRigBlueprint.h" -#include "ControlRigGraphSchema.h" +#include "Graph/ControlRigGraphSchema.h" UControlRigGraph::UControlRigGraph() { diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphNode.cpp similarity index 94% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.cpp rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphNode.cpp index 403c18dd802e..12c17534a17b 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphNode.cpp @@ -1,8 +1,9 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. -#include "ControlRigGraphNode.h" -#include "ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" #include "EdGraph/EdGraphPin.h" +#include "Graph/ControlRigGraph.h" +#include "Graph/ControlRigGraphSchema.h" #include "Kismet2/BlueprintEditorUtils.h" #include "KismetCompiler.h" #include "BlueprintNodeSpawner.h" @@ -11,11 +12,7 @@ #include "ControlRig.h" #include "Textures/SlateIcon.h" #include "Units/RigUnit.h" -#include "ControlRigGraphSchema.h" #include "ControlRigBlueprint.h" -#include "ControlRigBlueprintCommands.h" -#include "Framework/MultiBox/MultiBoxBuilder.h" -#include "SControlRig.h" #include "PropertyPathHelpers.h" #include "Kismet2/Kismet2NameValidators.h" #include "ScopedTransaction.h" @@ -23,6 +20,10 @@ #include "UObject/PropertyPortFlags.h" #include "ControlRigBlueprintUtils.h" +#if WITH_EDITOR +#include "IControlRigEditorModule.h" +#endif //WITH_EDITOR + #define LOCTEXT_NAMESPACE "ControlRigGraphNode" FText UControlRigGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const @@ -211,6 +212,56 @@ void UControlRigGraphNode::CreateVariablePins(bool bAlwaysCreatePins) CreateOutputPins(bAlwaysCreatePins); } +void UControlRigGraphNode::HandleClearArray(FString InPropertyPath) +{ + const FScopedTransaction Transaction(LOCTEXT("ClearArrayTransaction", "Clear Array")); + + if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) + { + InArrayHelper.EmptyValues(); + return true; + }, true, true)) + { + ReconstructNode(); + } +} + +void UControlRigGraphNode::HandleRemoveArrayElement(FString InPropertyPath) +{ + const FScopedTransaction Transaction(LOCTEXT("RemoveArrayElementTransaction", "Remove Array Element")); + + if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) + { + if(InArrayIndex != INDEX_NONE) + { + InArrayHelper.RemoveValues(InArrayIndex); + return true; + } + return false; + }, true, true)) + { + ReconstructNode(); + } +} + +void UControlRigGraphNode::HandleInsertArrayElement(FString InPropertyPath) +{ + const FScopedTransaction Transaction(LOCTEXT("InsertArrayElementTransaction", "Insert Array Element")); + + if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) + { + if(InArrayIndex != INDEX_NONE) + { + InArrayHelper.InsertValues(InArrayIndex); + return true; + } + return false; + }, true, true)) + { + ReconstructNode(); + } +} + void UControlRigGraphNode::AllocateDefaultPins() { CreateVariablePins(true); @@ -637,51 +688,9 @@ void UControlRigGraphNode::PinConnectionListChanged(UEdGraphPin* Pin) void UControlRigGraphNode::GetContextMenuActions(const FGraphNodeContextMenuBuilder& Context) const { - if(Context.MenuBuilder != nullptr) - { - if(Context.Pin != nullptr) - { - // Add array operations for array pins - if(Context.Pin->PinType.IsArray()) - { - // End the section as this function is called with a section 'open' - Context.MenuBuilder->EndSection(); - - Context.MenuBuilder->BeginSection(TEXT("ArrayOperations"), LOCTEXT("ArrayOperations", "Array Operations")); - - // Array operations - Context.MenuBuilder->AddMenuEntry( - LOCTEXT("ClearArray", "Clear"), - LOCTEXT("ClearArray_Tooltip", "Clear this array of all of its entries"), - FSlateIcon(), - FUIAction(FExecuteAction::CreateUObject(this, &UControlRigGraphNode::HandleClearArray, Context.Pin->PinName.ToString()))); - - Context.MenuBuilder->EndSection(); - } - else if(Context.Pin->ParentPin != nullptr && Context.Pin->ParentPin->PinType.IsArray()) - { - // End the section as this function is called with a section 'open' - Context.MenuBuilder->EndSection(); - - Context.MenuBuilder->BeginSection(TEXT("ArrayElementOperations"), LOCTEXT("ArrayElementOperations", "Array Element Operations")); - - // Array element operations - Context.MenuBuilder->AddMenuEntry( - LOCTEXT("RemoveArrayElement", "Remove"), - LOCTEXT("RemoveArrayElement_Tooltip", "Remove this array element"), - FSlateIcon(), - FUIAction(FExecuteAction::CreateUObject(this, &UControlRigGraphNode::HandleRemoveArrayElement, Context.Pin->PinName.ToString()))); - - Context.MenuBuilder->AddMenuEntry( - LOCTEXT("InsertArrayElement", "Insert"), - LOCTEXT("InsertArrayElement_Tooltip", "Insert an array element after this one"), - FSlateIcon(), - FUIAction(FExecuteAction::CreateUObject(this, &UControlRigGraphNode::HandleInsertArrayElement, Context.Pin->PinName.ToString()))); - - Context.MenuBuilder->EndSection(); - } - } - } +#if WITH_EDITOR + IControlRigEditorModule::Get().GetContextMenuActions(this, Context); +#endif } void UControlRigGraphNode::SetPinExpansion(const FString& InPinPropertyPath, bool bExpanded) @@ -999,56 +1008,6 @@ void UControlRigGraphNode::HandleAddArrayElement(FString InPropertyPath) } } -void UControlRigGraphNode::HandleClearArray(FString InPropertyPath) -{ - const FScopedTransaction Transaction(LOCTEXT("ClearArrayTransaction", "Clear Array")); - - if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) - { - InArrayHelper.EmptyValues(); - return true; - }, true, true)) - { - ReconstructNode(); - } -} - -void UControlRigGraphNode::HandleRemoveArrayElement(FString InPropertyPath) -{ - const FScopedTransaction Transaction(LOCTEXT("RemoveArrayElementTransaction", "Remove Array Element")); - - if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) - { - if(InArrayIndex != INDEX_NONE) - { - InArrayHelper.RemoveValues(InArrayIndex); - return true; - } - return false; - }, true, true)) - { - ReconstructNode(); - } -} - -void UControlRigGraphNode::HandleInsertArrayElement(FString InPropertyPath) -{ - const FScopedTransaction Transaction(LOCTEXT("InsertArrayElementTransaction", "Insert Array Element")); - - if(PerformArrayOperation(InPropertyPath, [](FScriptArrayHelper& InArrayHelper, int32 InArrayIndex) - { - if(InArrayIndex != INDEX_NONE) - { - InArrayHelper.InsertValues(InArrayIndex); - return true; - } - return false; - }, true, true)) - { - ReconstructNode(); - } -} - void ReplacePropertyName(TArray>& InArray, const FString& OldPropName, const FString& NewPropName) { for (int32 Index = 0; Index < InArray.Num(); ++Index) diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphSchema.cpp similarity index 91% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.cpp rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphSchema.cpp index 536ae21884dc..8f15755224e4 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Private/Graph/ControlRigGraphSchema.cpp @@ -1,12 +1,11 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. -#include "ControlRigGraphSchema.h" -#include "ControlRigGraph.h" -#include "ControlRigGraphNode.h" -#include "ControlRigConnectionDrawingPolicy.h" +#include "Graph/ControlRigGraphSchema.h" +#include "Graph/ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" +#include "IControlRigEditorModule.h" #include "UObject/UObjectIterator.h" #include "Units/RigUnit.h" -#include "ControlRigBlueprintUtils.h" #include "Kismet2/BlueprintEditorUtils.h" #include "ScopedTransaction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" @@ -27,27 +26,11 @@ void UControlRigGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& Co void UControlRigGraphSchema::GetContextMenuActions(const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) const { - if(MenuBuilder) - { - MenuBuilder->BeginSection("ContextMenu"); - - UEdGraphSchema::GetContextMenuActions(CurrentGraph, InGraphNode, InGraphPin, MenuBuilder, bIsDebugging); - - MenuBuilder->EndSection(); - - if (InGraphPin != NULL) - { - MenuBuilder->BeginSection("EdGraphSchemaPinActions", LOCTEXT("PinActionsMenuHeader", "Pin Actions")); - { - // Break pin links - if (InGraphPin->LinkedTo.Num() > 0) - { - MenuBuilder->AddMenuEntry( FGraphEditorCommands::Get().BreakPinLinks ); - } - } - MenuBuilder->EndSection(); - } - } +#if WITH_EDITOR + return IControlRigEditorModule::Get().GetContextMenuActions(this, CurrentGraph, InGraphNode, InGraphPin, MenuBuilder, bIsDebugging); +#else + check(0); +#endif } bool UControlRigGraphSchema::TryCreateConnection_Extended(UEdGraphPin* PinA, UEdGraphPin* PinB) const @@ -260,7 +243,12 @@ void UControlRigGraphSchema::BreakSinglePinLink(UEdGraphPin* SourcePin, UEdGraph FConnectionDrawingPolicy* UControlRigGraphSchema::CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const { - return new FControlRigConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj); +#if WITH_EDITOR + return IControlRigEditorModule::Get().CreateConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj); +#else + check(0); + return nullptr; +#endif } bool UControlRigGraphSchema::ShouldHidePinDefaultValue(UEdGraphPin* Pin) const diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprint.h similarity index 90% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprint.h index 21475ea01ce9..a60e9164a978 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprint.h @@ -9,16 +9,20 @@ #include "Misc/Crc.h" #include "ControlRigDefines.h" #include "Hierarchy.h" -#include "ControlRigPickerWidget.h" #include "Interfaces/Interface_PreviewMeshProvider.h" #include "ControlRigBlueprint.generated.h" class UControlRigBlueprintGeneratedClass; class USkeletalMesh; +/** + * Source data used by the FControlRigBlueprintCompiler, can't be an editor plugin + * because it is needed when running with -game. + */ + /** A link between two properties. Links become copies between property data at runtime. */ USTRUCT() -struct FControlRigBlueprintPropertyLink +struct CONTROLRIGDEVELOPER_API FControlRigBlueprintPropertyLink { GENERATED_BODY() @@ -63,7 +67,7 @@ private: }; UCLASS(BlueprintType) -class UControlRigBlueprint : public UBlueprint, public IInterface_PreviewMeshProvider +class CONTROLRIGDEVELOPER_API UControlRigBlueprint : public UBlueprint, public IInterface_PreviewMeshProvider { GENERATED_BODY() @@ -89,9 +93,6 @@ public: /** Make a property link between the specified properties - used by the compiler */ void MakePropertyLink(const FString& InSourcePropertyPath, const FString& InDestPropertyPath); - /** Get the picker widget class for this rig */ - TSubclassOf GetPickerWidgetClass() const { return PickerWidgetClass; } - /** IInterface_PreviewMeshProvider interface */ virtual void SetPreviewMesh(USkeletalMesh* PreviewMesh, bool bMarkAsDirty = true) override; virtual USkeletalMesh* GetPreviewMesh() const override; @@ -113,10 +114,6 @@ private: UPROPERTY(VisibleAnywhere, Category = "Hierarchy") FRigHierarchy Hierarchy; - /** The picker widget class */ - UPROPERTY(EditAnywhere, Category="Picker") - TSubclassOf PickerWidgetClass; - /** The default skeletal mesh to use when previewing this asset */ UPROPERTY(DuplicateTransient, AssetRegistrySearchable) TSoftObjectPtr PreviewSkeletalMesh; diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintCompiler.h similarity index 90% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintCompiler.h index f496474db6fd..54e3de286023 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprintCompiler.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintCompiler.h @@ -8,7 +8,7 @@ struct FControlRigBlueprintPropertyLink; class UControlRigBlueprintGeneratedClass; -class FControlRigBlueprintCompiler : public IBlueprintCompiler +class CONTROLRIGDEVELOPER_API FControlRigBlueprintCompiler : public IBlueprintCompiler { public: /** IBlueprintCompiler interface */ @@ -16,7 +16,7 @@ public: virtual void Compile(UBlueprint* Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results, TArray* ObjLoaded) override; }; -class FControlRigBlueprintCompilerContext : public FKismetCompilerContext +class CONTROLRIGDEVELOPER_API FControlRigBlueprintCompilerContext : public FKismetCompilerContext { public: FControlRigBlueprintCompilerContext(UBlueprint* SourceSketch, FCompilerResultsLog& InMessageLog, const FKismetCompilerOptions& InCompilerOptions, TArray* InObjLoaded) diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintUtils.h similarity index 98% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintUtils.h index d27fbdbc21f8..ddc242f85a89 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigBlueprintUtils.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigBlueprintUtils.h @@ -12,7 +12,7 @@ class UControlRigGraphNode; class UEdGraph; class UEdGraphPin; -struct FControlRigBlueprintUtils +struct CONTROLRIGDEVELOPER_API FControlRigBlueprintUtils { /** diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigDeveloper.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigDeveloper.h new file mode 100644 index 000000000000..cd9b7380789a --- /dev/null +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/ControlRigDeveloper.h @@ -0,0 +1,12 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" + +class IControlRigDeveloperModule : public IModuleInterface +{ +public: +}; + diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraph.h similarity index 87% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraph.h index d34d38eb3e92..a6b53ce4ce0f 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraph.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraph.h @@ -9,7 +9,7 @@ class UControlRigBlueprint; class UControlRigGraphSchema; UCLASS() -class UControlRigGraph : public UEdGraph +class CONTROLRIGDEVELOPER_API UControlRigGraph : public UEdGraph { GENERATED_BODY() diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphNode.h similarity index 99% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphNode.h index 13cf8bbe8129..21043aacf858 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphNode.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphNode.h @@ -116,7 +116,7 @@ private: /** Base class for animation ControlRig-related nodes */ UCLASS() -class UControlRigGraphNode : public UEdGraphNode +class CONTROLRIGDEVELOPER_API UControlRigGraphNode : public UEdGraphNode { GENERATED_BODY() @@ -223,7 +223,15 @@ public: /** Create Variable Pins on the side */ void CreateVariablePins(bool bAlwaysCreatePins = false); + + /** Clear the array referred to by the property path */ + void HandleClearArray(FString InPropertyPath); + /** Remove the array element referred to by the property path */ + void HandleRemoveArrayElement(FString InPropertyPath); + + /** Insert a new array element after the element referred to by the property path */ + void HandleInsertArrayElement(FString InPropertyPath); protected: /** Rebuild the cached info about our inputs/outputs */ void CacheVariableInfo(); @@ -298,13 +306,4 @@ protected: * If bCallModify is true then it is assumed that the array will be mutated. */ bool PerformArrayOperation(const FString& InPropertyPath, TFunctionRef InOperation, bool bCallModify, bool bPropagateToInstances) const; - - /** Clear the array referred to by the property path */ - void HandleClearArray(FString InPropertyPath); - - /** Remove the array element referred to by the property path */ - void HandleRemoveArrayElement(FString InPropertyPath); - - /** Insert a new array element after the element referred to by the property path */ - void HandleInsertArrayElement(FString InPropertyPath); }; diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphSchema.h similarity index 97% rename from Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.h rename to Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphSchema.h index 9f1da5ae6e3a..c54ec528ab91 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphSchema.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigDeveloper/Public/Graph/ControlRigGraphSchema.h @@ -40,7 +40,7 @@ struct FControlRigPinConnectionResponse }; UCLASS() -class UControlRigGraphSchema : public UEdGraphSchema +class CONTROLRIGDEVELOPER_API UControlRigGraphSchema : public UEdGraphSchema { GENERATED_BODY() diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/ControlRigEditor.Build.cs b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/ControlRigEditor.Build.cs index 90aef12b87b1..c66d584b38c3 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/ControlRigEditor.Build.cs +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/ControlRigEditor.Build.cs @@ -25,6 +25,7 @@ namespace UnrealBuildTool.Rules "KismetCompiler", "BlueprintGraph", "ControlRig", + "ControlRigDeveloper", "Kismet", "EditorStyle", "AnimationCore", diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.cpp deleted file mode 100644 index c9265f68873b..000000000000 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigBlueprint.cpp +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. - -#include "ControlRigBlueprint.h" -#include "ControlRigBlueprintGeneratedClass.h" -#include "EdGraph/EdGraph.h" -#include "ControlRigGraphNode.h" -#include "ControlRigGraphSchema.h" -#include "Modules/ModuleManager.h" -#include "Engine/SkeletalMesh.h" -#include "BlueprintActionDatabaseRegistrar.h" -#include "BlueprintNodeSpawner.h" -#include "ControlRigBlueprintUtils.h" -#include "NodeSpawners/ControlRigPropertyNodeSpawner.h" -#include "NodeSpawners/ControlRigUnitNodeSpawner.h" -#include "NodeSpawners/ControlRigVariableNodeSpawner.h" - -#define LOCTEXT_NAMESPACE "ControlRigBlueprint" -UControlRigBlueprint::UControlRigBlueprint() -{ -} - -UControlRigBlueprintGeneratedClass* UControlRigBlueprint::GetControlRigBlueprintGeneratedClass() const -{ - UControlRigBlueprintGeneratedClass* Result = Cast(*GeneratedClass); - return Result; -} - -UControlRigBlueprintGeneratedClass* UControlRigBlueprint::GetControlRigBlueprintSkeletonClass() const -{ - UControlRigBlueprintGeneratedClass* Result = Cast(*SkeletonGeneratedClass); - return Result; -} - -#if WITH_EDITOR - -UClass* UControlRigBlueprint::GetBlueprintClass() const -{ - return UControlRigBlueprintGeneratedClass::StaticClass(); -} - -void UControlRigBlueprint::LoadModulesRequiredForCompilation() -{ - static const FName ModuleName(TEXT("ControlRigEditor")); - FModuleManager::Get().LoadModule(ModuleName); -} -#endif - -void UControlRigBlueprint::MakePropertyLink(const FString& InSourcePropertyPath, const FString& InDestPropertyPath) -{ - PropertyLinks.AddUnique(FControlRigBlueprintPropertyLink(InSourcePropertyPath, InDestPropertyPath)); -} - -USkeletalMesh* UControlRigBlueprint::GetPreviewMesh() const -{ - if (!PreviewSkeletalMesh.IsValid()) - { - PreviewSkeletalMesh.LoadSynchronous(); - } - - return PreviewSkeletalMesh.Get(); -} - -void UControlRigBlueprint::SetPreviewMesh(USkeletalMesh* PreviewMesh, bool bMarkAsDirty/*=true*/) -{ - if(bMarkAsDirty) - { - Modify(); - } - - PreviewSkeletalMesh = PreviewMesh; -} - -void UControlRigBlueprint::GetTypeActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const -{ - // actions get registered under specific object-keys; the idea is that - // actions might have to be updated (or deleted) if their object-key is - // mutated (or removed)... here we use the class (so if the class - // type disappears, then the action should go with it) - UClass* ActionKey = GetClass(); - // to keep from needlessly instantiating a UBlueprintNodeSpawner, first - // check to make sure that the registrar is looking for actions of this type - // (could be regenerating actions for a specific asset, and therefore the - // registrar would only accept actions corresponding to that asset) - if (!ActionRegistrar.IsOpenForRegistration(ActionKey)) - { - return; - } - - // Add all rig units - FControlRigBlueprintUtils::ForAllRigUnits([&](UStruct* InStruct) - { - FText NodeCategory = FText::FromString(InStruct->GetMetaData(TEXT("Category"))); - FText MenuDesc = FText::FromString(InStruct->GetMetaData(TEXT("DisplayName"))); - FText ToolTip = InStruct->GetToolTipText(); - - UBlueprintNodeSpawner* NodeSpawner = UControlRigUnitNodeSpawner::CreateFromStruct(InStruct, MenuDesc, NodeCategory, ToolTip); - check(NodeSpawner != nullptr); - ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); - }); - - // Add 'new properties' - TArray> PinTypes; - GetDefault()->GetVariableTypeTree(PinTypes, ETypeTreeFilter::None); - - struct Local - { - static void AddVariableActions_Recursive(UClass* InActionKey, FBlueprintActionDatabaseRegistrar& InActionRegistrar, const TSharedPtr& InPinTypeTreeItem, const FString& InCurrentCategory) - { - static const FString CategoryDelimiter(TEXT("|")); - - if (InPinTypeTreeItem->Children.Num() == 0) - { - FText NodeCategory = FText::FromString(InCurrentCategory); - FText MenuDesc = InPinTypeTreeItem->GetDescription(); - FText ToolTip = InPinTypeTreeItem->GetToolTip(); - - UBlueprintNodeSpawner* NodeSpawner = UControlRigVariableNodeSpawner::CreateFromPinType(InPinTypeTreeItem->GetPinType(false), MenuDesc, NodeCategory, ToolTip); - check(NodeSpawner != nullptr); - InActionRegistrar.AddBlueprintAction(InActionKey, NodeSpawner); - } - else - { - FString CurrentCategory = InCurrentCategory + CategoryDelimiter + InPinTypeTreeItem->FriendlyName.ToString(); - - for (const TSharedPtr& ChildTreeItem : InPinTypeTreeItem->Children) - { - AddVariableActions_Recursive(InActionKey, InActionRegistrar, ChildTreeItem, CurrentCategory); - } - } - } - }; - - FString CurrentCategory = LOCTEXT("NewVariable", "New Variable").ToString(); - for (const TSharedPtr& PinTypeTreeItem : PinTypes) - { - Local::AddVariableActions_Recursive(ActionKey, ActionRegistrar, PinTypeTreeItem, CurrentCategory); - } -} - -void UControlRigBlueprint::GetInstanceActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const -{ - // actions get registered under specific object-keys; the idea is that - // actions might have to be updated (or deleted) if their object-key is - // mutated (or removed)... here we use the generated class (so if the class - // type disappears, then the action should go with it) - UClass* ActionKey = GeneratedClass; - // to keep from needlessly instantiating a UBlueprintNodeSpawner, first - // check to make sure that the registrar is looking for actions of this type - // (could be regenerating actions for a specific asset, and therefore the - // registrar would only accept actions corresponding to that asset) - if (!ActionRegistrar.IsOpenForRegistration(ActionKey)) - { - return; - } - - for (TFieldIterator PropertyIt(ActionKey, EFieldIteratorFlags::ExcludeSuper); PropertyIt; ++PropertyIt) - { - UBlueprintNodeSpawner* NodeSpawner = UControlRigPropertyNodeSpawner::CreateFromProperty(UControlRigGraphNode::StaticClass(), *PropertyIt); - ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); - } -} -#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.cpp index cabedae778e0..b9c525b83fc3 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.cpp @@ -1,12 +1,16 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigEditorModule.h" -#include "PropertyEditorModule.h" -#include "ControlRigVariableDetailsCustomization.h" +#include "BlueprintActionDatabaseRegistrar.h" #include "BlueprintEditorModule.h" -#include "Kismet2/KismetEditorUtilities.h" +#include "BlueprintNodeSpawner.h" +#include "PropertyEditorModule.h" #include "ControlRig.h" #include "ControlRigComponent.h" +#include "ControlRigConnectionDrawingPolicy.h" +#include "ControlRigVariableDetailsCustomization.h" +#include "GraphEditorActions.h" +#include "Kismet2/KismetEditorUtilities.h" #include "ISequencerModule.h" #include "ControlRigTrackEditor.h" #include "IAssetTools.h" @@ -46,8 +50,11 @@ #include "ControlRigBlueprintActions.h" #include "Graph/ControlRigGraphSchema.h" #include "Graph/ControlRigGraph.h" +#include "Graph/NodeSpawners/ControlRigPropertyNodeSpawner.h" +#include "Graph/NodeSpawners/ControlRigUnitNodeSpawner.h" +#include "Graph/NodeSpawners/ControlRigVariableNodeSpawner.h" #include "Kismet2/BlueprintEditorUtils.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "EdGraphUtilities.h" #include "ControlRigGraphPanelNodeFactory.h" #include "ControlRigGraphPanelPinFactory.h" @@ -717,6 +724,175 @@ void FControlRigEditorModule::UnregisterRigUnitEditorClass(FName RigUnitClassNam RigUnitEditorClasses.Remove(RigUnitClassName); } +void FControlRigEditorModule::GetTypeActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) +{ + // actions get registered under specific object-keys; the idea is that + // actions might have to be updated (or deleted) if their object-key is + // mutated (or removed)... here we use the class (so if the class + // type disappears, then the action should go with it) + UClass* ActionKey = CRB->GetClass(); + // to keep from needlessly instantiating a UBlueprintNodeSpawner, first + // check to make sure that the registrar is looking for actions of this type + // (could be regenerating actions for a specific asset, and therefore the + // registrar would only accept actions corresponding to that asset) + if (!ActionRegistrar.IsOpenForRegistration(ActionKey)) + { + return; + } + + // Add all rig units + FControlRigBlueprintUtils::ForAllRigUnits([&](UStruct* InStruct) + { + FText NodeCategory = FText::FromString(InStruct->GetMetaData(TEXT("Category"))); + FText MenuDesc = FText::FromString(InStruct->GetMetaData(TEXT("DisplayName"))); + FText ToolTip = InStruct->GetToolTipText(); + + UBlueprintNodeSpawner* NodeSpawner = UControlRigUnitNodeSpawner::CreateFromStruct(InStruct, MenuDesc, NodeCategory, ToolTip); + check(NodeSpawner != nullptr); + ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); + }); + + // Add 'new properties' + TArray> PinTypes; + GetDefault()->GetVariableTypeTree(PinTypes, ETypeTreeFilter::None); + + struct Local + { + static void AddVariableActions_Recursive(UClass* InActionKey, FBlueprintActionDatabaseRegistrar& InActionRegistrar, const TSharedPtr& InPinTypeTreeItem, const FString& InCurrentCategory) + { + static const FString CategoryDelimiter(TEXT("|")); + + if (InPinTypeTreeItem->Children.Num() == 0) + { + FText NodeCategory = FText::FromString(InCurrentCategory); + FText MenuDesc = InPinTypeTreeItem->GetDescription(); + FText ToolTip = InPinTypeTreeItem->GetToolTip(); + + UBlueprintNodeSpawner* NodeSpawner = UControlRigVariableNodeSpawner::CreateFromPinType(InPinTypeTreeItem->GetPinType(false), MenuDesc, NodeCategory, ToolTip); + check(NodeSpawner != nullptr); + InActionRegistrar.AddBlueprintAction(InActionKey, NodeSpawner); + } + else + { + FString CurrentCategory = InCurrentCategory + CategoryDelimiter + InPinTypeTreeItem->FriendlyName.ToString(); + + for (const TSharedPtr& ChildTreeItem : InPinTypeTreeItem->Children) + { + AddVariableActions_Recursive(InActionKey, InActionRegistrar, ChildTreeItem, CurrentCategory); + } + } + } + }; + + FString CurrentCategory = LOCTEXT("NewVariable", "New Variable").ToString(); + for (const TSharedPtr& PinTypeTreeItem : PinTypes) + { + Local::AddVariableActions_Recursive(ActionKey, ActionRegistrar, PinTypeTreeItem, CurrentCategory); + } +} + +void FControlRigEditorModule::GetInstanceActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) +{ + // actions get registered under specific object-keys; the idea is that + // actions might have to be updated (or deleted) if their object-key is + // mutated (or removed)... here we use the generated class (so if the class + // type disappears, then the action should go with it) + UClass* ActionKey = CRB->GeneratedClass; + // to keep from needlessly instantiating a UBlueprintNodeSpawner, first + // check to make sure that the registrar is looking for actions of this type + // (could be regenerating actions for a specific asset, and therefore the + // registrar would only accept actions corresponding to that asset) + if (!ActionRegistrar.IsOpenForRegistration(ActionKey)) + { + return; + } + + for (TFieldIterator PropertyIt(ActionKey, EFieldIteratorFlags::ExcludeSuper); PropertyIt; ++PropertyIt) + { + UBlueprintNodeSpawner* NodeSpawner = UControlRigPropertyNodeSpawner::CreateFromProperty(UControlRigGraphNode::StaticClass(), *PropertyIt); + ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); + } +} + +FConnectionDrawingPolicy* FControlRigEditorModule::CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) +{ + return new FControlRigConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, InZoomFactor, InClippingRect, InDrawElements, InGraphObj); +} + +void FControlRigEditorModule::GetContextMenuActions(const UControlRigGraphNode* Node, const FGraphNodeContextMenuBuilder& Context ) +{ + if(Context.MenuBuilder != nullptr) + { + if(Context.Pin != nullptr) + { + // Add array operations for array pins + if(Context.Pin->PinType.IsArray()) + { + // End the section as this function is called with a section 'open' + Context.MenuBuilder->EndSection(); + + Context.MenuBuilder->BeginSection(TEXT("ArrayOperations"), LOCTEXT("ArrayOperations", "Array Operations")); + + // Array operations + Context.MenuBuilder->AddMenuEntry( + LOCTEXT("ClearArray", "Clear"), + LOCTEXT("ClearArray_Tooltip", "Clear this array of all of its entries"), + FSlateIcon(), + FUIAction(FExecuteAction::CreateUObject(Node, &UControlRigGraphNode::HandleClearArray, Context.Pin->PinName.ToString()))); + + Context.MenuBuilder->EndSection(); + } + else if(Context.Pin->ParentPin != nullptr && Context.Pin->ParentPin->PinType.IsArray()) + { + // End the section as this function is called with a section 'open' + Context.MenuBuilder->EndSection(); + + Context.MenuBuilder->BeginSection(TEXT("ArrayElementOperations"), LOCTEXT("ArrayElementOperations", "Array Element Operations")); + + // Array element operations + Context.MenuBuilder->AddMenuEntry( + LOCTEXT("RemoveArrayElement", "Remove"), + LOCTEXT("RemoveArrayElement_Tooltip", "Remove this array element"), + FSlateIcon(), + FUIAction(FExecuteAction::CreateUObject(Node, &UControlRigGraphNode::HandleRemoveArrayElement, Context.Pin->PinName.ToString()))); + + Context.MenuBuilder->AddMenuEntry( + LOCTEXT("InsertArrayElement", "Insert"), + LOCTEXT("InsertArrayElement_Tooltip", "Insert an array element after this one"), + FSlateIcon(), + FUIAction(FExecuteAction::CreateUObject(Node, &UControlRigGraphNode::HandleInsertArrayElement, Context.Pin->PinName.ToString()))); + + Context.MenuBuilder->EndSection(); + } + } + } +} + +void FControlRigEditorModule::GetContextMenuActions(const UControlRigGraphSchema* Schema, const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) +{ + if(MenuBuilder) + { + MenuBuilder->BeginSection("ContextMenu"); + + Schema->UEdGraphSchema::GetContextMenuActions(CurrentGraph, InGraphNode, InGraphPin, MenuBuilder, bIsDebugging); + + MenuBuilder->EndSection(); + + if (InGraphPin != NULL) + { + MenuBuilder->BeginSection("EdGraphSchemaPinActions", LOCTEXT("PinActionsMenuHeader", "Pin Actions")); + { + // Break pin links + if (InGraphPin->LinkedTo.Num() > 0) + { + MenuBuilder->AddMenuEntry( FGraphEditorCommands::Get().BreakPinLinks ); + } + } + MenuBuilder->EndSection(); + } + } +} + // It's CDO of the class, so we don't want the object to be writable or even if you write, it won't be per instance TSubclassOf FControlRigEditorModule::GetEditorObjectByRigUnit(const FName& RigUnitClassName) { diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.h index ae09a5798e95..fef73ea9420e 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/ControlRigEditorModule.h @@ -42,8 +42,15 @@ public: UMaterial* GetTrajectoryMaterial() const { return TrajectoryMaterial.Get(); } - virtual void RegisterRigUnitEditorClass(FName RigUnitClassName, TSubclassOf Class); - virtual void UnregisterRigUnitEditorClass(FName RigUnitClassName); + virtual void RegisterRigUnitEditorClass(FName RigUnitClassName, TSubclassOf Class) override; + virtual void UnregisterRigUnitEditorClass(FName RigUnitClassName) override; + + virtual void GetTypeActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) override; + virtual void GetInstanceActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) override; + virtual FConnectionDrawingPolicy* CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) override; + virtual void GetContextMenuActions(const UControlRigGraphNode* Node, const FGraphNodeContextMenuBuilder& Context ) override; + virtual void GetContextMenuActions(const UControlRigGraphSchema* Schema, const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) override; + static TSubclassOf GetEditorObjectByRigUnit(const FName& RigUnitClassName); private: diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SControlPicker.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SControlPicker.cpp index 5365d5c09902..164aef10b21a 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SControlPicker.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SControlPicker.cpp @@ -35,16 +35,6 @@ void SControlPicker::SetControlRig(UControlRig* InRig) if (InRig != RigPtr.Get()) { RigPtr = InRig; - - if(InRig) - { - UControlRigBlueprint* ControlRigBlueprint = CastChecked(CastChecked(InRig->GetClass())->ClassGeneratedBy); - EditorUserWidgetHost->SetUserWidgetClass(ControlRigBlueprint->GetPickerWidgetClass()); - } - else - { - EditorUserWidgetHost->SetUserWidgetClass(nullptr); - } } } diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.cpp index 6dbd652b0f33..74db5a59e255 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.cpp @@ -1,12 +1,12 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "SEditorUserWidgetHost.h" +#include "Engine/World.h" void SEditorUserWidgetHost::Construct(const FArguments& InArgs, UWorld* InWorld) { check(InWorld); World = InWorld; - UserWidget = nullptr; ChildSlot [ @@ -16,26 +16,4 @@ void SEditorUserWidgetHost::Construct(const FArguments& InArgs, UWorld* InWorld) void SEditorUserWidgetHost::AddReferencedObjects(FReferenceCollector& Collector) { - Collector.AddReferencedObject(UserWidget); } - -void SEditorUserWidgetHost::SetUserWidgetClass(TSubclassOf InUserWidgetClass) -{ - TSharedPtr Widget = SNullWidget::NullWidget; - // world is weak object ptr, if you want to make it strong, make sure it gets cleaned up - // before level shut down - if(InUserWidgetClass.Get() != nullptr && World.IsValid()) - { - UserWidget = CreateWidget(World.Get(), InUserWidgetClass); - Widget = UserWidget->TakeWidget(); - } - else - { - UserWidget = nullptr; - } - - ChildSlot - [ - Widget.ToSharedRef() - ]; -} \ No newline at end of file diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.h index faa82565f6f5..22257c9b8ac5 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/EditMode/SEditorUserWidgetHost.h @@ -2,8 +2,9 @@ #pragma once +#include "UObject/GCObject.h" #include "Widgets/SCompoundWidget.h" -#include "ControlRigPickerWidget.h" +#include "Widgets/DeclarativeSyntaxSupport.h" class SEditorUserWidgetHost : public SCompoundWidget, public FGCObject { @@ -17,13 +18,7 @@ public: /** FGCObject interface */ virtual void AddReferencedObjects( FReferenceCollector& Collector ) override; - /** Set a new user widget class */ - void SetUserWidgetClass(TSubclassOf InUserWidgetClass); - private: /** The world we create widgets with */ TWeakObjectPtr World; - - /** The user widget we are hosting */ - UControlRigPickerWidget* UserWidget; }; \ No newline at end of file diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigEditor.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigEditor.cpp index 0a8a181af0d0..5faaf061ef69 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigEditor.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/ControlRigEditor.cpp @@ -9,7 +9,7 @@ #include "SKismetInspector.h" #include "Framework/Commands/GenericCommands.h" #include "Editor.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "BlueprintActionDatabase.h" #include "ControlRigBlueprintCommands.h" #include "SControlRig.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRig.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRig.cpp index 373e94441c13..39ef323e6a12 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRig.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRig.cpp @@ -17,11 +17,11 @@ #include "ControlRigBlueprintUtils.h" #include "NodeSpawners/ControlRigPropertyNodeSpawner.h" #include "SControlRigItem.h" -#include "ControlRigBlueprintCommands.h" -#include "ControlRigGraph.h" #include "ControlRigBlueprint.h" -#include "ControlRigGraphNode.h" -#include "ControlRigGraphSchema.h" +#include "ControlRigBlueprintCommands.h" +#include "Graph/ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" +#include "Graph/ControlRigGraphSchema.h" #include "GraphEditorModule.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Algo/Transform.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRigUnitCombo.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRigUnitCombo.cpp index 281297125c26..b4754dd99e5a 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRigUnitCombo.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SControlRigUnitCombo.cpp @@ -15,7 +15,7 @@ #include "EditorStyleSet.h" #include "ControlRigBlueprintUtils.h" #include "SGraphEditorActionMenu.h" -#include "ControlRigGraph.h" +#include "Graph/ControlRigGraph.h" #define LOCTEXT_NAMESPACE "SControlRigUnitCombo" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SRigHierarchy.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SRigHierarchy.cpp index ebc01ae96f22..7b9e499c3e27 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SRigHierarchy.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Editor/SRigHierarchy.cpp @@ -16,10 +16,10 @@ #include "ControlRigBlueprintUtils.h" #include "NodeSpawners/ControlRigPropertyNodeSpawner.h" #include "ControlRigHierarchyCommands.h" -#include "ControlRigGraph.h" #include "ControlRigBlueprint.h" -#include "ControlRigGraphNode.h" -#include "ControlRigGraphSchema.h" +#include "Graph/ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" +#include "Graph/ControlRigGraphSchema.h" #include "GraphEditorModule.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "AnimationRuntime.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelNodeFactory.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelNodeFactory.cpp index 24f75db8edf9..b3de31fdbf7a 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelNodeFactory.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelNodeFactory.cpp @@ -1,8 +1,8 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigGraphPanelNodeFactory.h" -#include "ControlRigGraphNode.h" -#include "ControlRigGraph.h" +#include "Graph/ControlRigGraphNode.h" +#include "Graph/ControlRigGraph.h" #include "SControlRigGraphNode.h" TSharedPtr FControlRigGraphPanelNodeFactory::CreateNode(UEdGraphNode* Node) const diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelPinFactory.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelPinFactory.cpp index f594965fd3b6..52eea7a9fdb2 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelPinFactory.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/ControlRigGraphPanelPinFactory.cpp @@ -1,7 +1,7 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigGraphPanelPinFactory.h" -#include "ControlRigGraphSchema.h" +#include "Graph/ControlRigGraphSchema.h" #include "NodeFactory.h" TSharedPtr FControlRigGraphPanelPinFactory::CreatePin(UEdGraphPin* InPin) const diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigPropertyNodeSpawner.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigPropertyNodeSpawner.cpp index f189a2022671..1908e3e8e557 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigPropertyNodeSpawner.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigPropertyNodeSpawner.cpp @@ -1,7 +1,7 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigPropertyNodeSpawner.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "EdGraphSchema_K2.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Classes/EditorStyleSettings.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigUnitNodeSpawner.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigUnitNodeSpawner.cpp index 2b9309659345..18b75f6dc2a9 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigUnitNodeSpawner.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigUnitNodeSpawner.cpp @@ -1,7 +1,7 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigUnitNodeSpawner.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "EdGraphSchema_K2.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Classes/EditorStyleSettings.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigVariableNodeSpawner.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigVariableNodeSpawner.cpp index d8aaa84ce5f7..d9467f492285 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigVariableNodeSpawner.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/NodeSpawners/ControlRigVariableNodeSpawner.cpp @@ -1,7 +1,7 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "ControlRigVariableNodeSpawner.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "EdGraphSchema_K2.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Classes/EditorStyleSettings.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/SControlRigGraphNode.cpp b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/SControlRigGraphNode.cpp index 9e6c029619e5..a190352849b7 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/SControlRigGraphNode.cpp +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Private/Graph/SControlRigGraphNode.cpp @@ -1,9 +1,9 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "SControlRigGraphNode.h" -#include "ControlRigGraphNode.h" +#include "Graph/ControlRigGraphNode.h" #include "SGraphPin.h" -#include "ControlRigGraphSchema.h" +#include "Graph/ControlRigGraphSchema.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SSpacer.h" diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditor.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditor.h index ce8583458867..61535c905ece 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditor.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditor.h @@ -5,6 +5,9 @@ #include "CoreMinimal.h" #include "BlueprintEditor.h" +class UControlRigBlueprint; +class FBlueprintActionDatabaseRegistrar; + class IControlRigEditor : public FBlueprintEditor { }; diff --git a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditorModule.h b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditorModule.h index 87efbe6ea3cc..281feb4def83 100644 --- a/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditorModule.h +++ b/Engine/Plugins/Experimental/ControlRig/Source/ControlRigEditor/Public/IControlRigEditorModule.h @@ -13,11 +13,19 @@ DECLARE_LOG_CATEGORY_EXTERN(LogControlRigEditor, Log, All); class IToolkitHost; class UControlRigBlueprint; +class UControlRigGraphNode; +class UControlRigGraphSchema; class URigUnitEditor_Base; +class FConnectionDrawingPolicy; class IControlRigEditorModule : public IModuleInterface, public IHasMenuExtensibility, public IHasToolBarExtensibility { public: + static FORCEINLINE IControlRigEditorModule& Get() + { + return FModuleManager::LoadModuleChecked< IControlRigEditorModule >(TEXT("ControlRigEditor")); + } + /** * Creates an instance of a Control Rig editor. * @@ -35,4 +43,9 @@ public: virtual void RegisterRigUnitEditorClass(FName RigUnitClassName, TSubclassOf Class) = 0; virtual void UnregisterRigUnitEditorClass(FName RigUnitClassName) = 0; + virtual void GetTypeActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) = 0; + virtual void GetInstanceActions(const UControlRigBlueprint* CRB, FBlueprintActionDatabaseRegistrar& ActionRegistrar) = 0; + virtual FConnectionDrawingPolicy* CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) = 0; + virtual void GetContextMenuActions(const UControlRigGraphNode* Node, const FGraphNodeContextMenuBuilder& Context ) = 0; + virtual void GetContextMenuActions(const UControlRigGraphSchema* Schema, const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) = 0; }; diff --git a/Engine/Plugins/Experimental/GeometryCache/Source/GeometryCache/Private/StreamingGeometryCacheData.cpp b/Engine/Plugins/Experimental/GeometryCache/Source/GeometryCache/Private/StreamingGeometryCacheData.cpp index a9602a8286b8..dd7899ab3c01 100644 --- a/Engine/Plugins/Experimental/GeometryCache/Source/GeometryCache/Private/StreamingGeometryCacheData.cpp +++ b/Engine/Plugins/Experimental/GeometryCache/Source/GeometryCache/Private/StreamingGeometryCacheData.cpp @@ -229,7 +229,7 @@ void FStreamingGeometryCacheData::UpdateStreamingStatus() FResidentChunk &ResidentChunk = AddResidentChunk(NeededIndex, Chunk); // Todo find something more smart... - EAsyncIOPriority AsyncIOPriority = AIOP_BelowNormal; + EAsyncIOPriorityAndFlags AsyncIOPriority = AIOP_BelowNormal; // Kick of a load if (!IORequestHandle) diff --git a/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyOnlineDocsWriter.cpp b/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyOnlineDocsWriter.cpp index 1ef85a874163..52693a291c98 100644 --- a/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyOnlineDocsWriter.cpp +++ b/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyOnlineDocsWriter.cpp @@ -368,6 +368,7 @@ void FPyOnlineDocsWriter::GenerateFiles(const FString& InPythonStubPath) "This can take a long time - 16+ minutes for full build on test system...\n"), *PyCommandStr); +#if !NO_LOGGING bool bLogSphinx = Commandline.Contains(TEXT("-HTMLLog")); ELogVerbosity::Type OldVerbosity = LogPython.GetVerbosity(); @@ -376,15 +377,18 @@ void FPyOnlineDocsWriter::GenerateFiles(const FString& InPythonStubPath) // Disable Python logging (default) LogPython.SetVerbosity(ELogVerbosity::NoLogging); } +#endif // !NO_LOGGING // Run the Python commands bool PyRunSuccess = FPythonScriptPlugin::Get()->RunString(*PyCommandStr); +#if !NO_LOGGING if (!bLogSphinx) { // Re-enable Python logging LogPython.SetVerbosity(OldVerbosity); } +#endif // !NO_LOGGING // The running of the Python commands seem to think there are errors no matter what so not much use to make this notification. //if (PyRunSuccess) diff --git a/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyUtil.cpp b/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyUtil.cpp index 2b7d964bf8fa..ef856cf9aede 100644 --- a/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyUtil.cpp +++ b/Engine/Plugins/Experimental/PythonScriptPlugin/Source/PythonScriptPlugin/Private/PyUtil.cpp @@ -539,7 +539,9 @@ bool InvokeFunctionCall(UObject* InObj, const UFunction* InFunc, void* InBasePar } else { +#if !NO_LOGGING FMsg::Logf_Internal(__FILE__, __LINE__, LogPython.GetCategoryName(), Verbosity, TEXT("%s"), ExceptionMessage); +#endif } }); diff --git a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalMeshReductionPlugin.cpp b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalMeshReductionPlugin.cpp index ad5ee74efa08..494725e75e8a 100644 --- a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalMeshReductionPlugin.cpp +++ b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalMeshReductionPlugin.cpp @@ -51,6 +51,40 @@ public: */ virtual bool IsSupported() const { return true; } + /** + * Returns true if mesh reduction is active. Active mean there will be a reduction of the vertices or triangle number + */ + virtual bool IsReductionActive(const struct FMeshReductionSettings &ReductionSettings) const + { + return false; + } + + virtual bool IsReductionActive(const FSkeletalMeshOptimizationSettings &ReductionSettings) const + { + float Threshold_One = (1.0f - KINDA_SMALL_NUMBER); + float Threshold_Zero = (0.0f + KINDA_SMALL_NUMBER); + switch (ReductionSettings.TerminationCriterion) + { + case SkeletalMeshTerminationCriterion::SMTC_NumOfTriangles: + { + return ReductionSettings.NumOfTrianglesPercentage < Threshold_One; + } + break; + case SkeletalMeshTerminationCriterion::SMTC_NumOfVerts: + { + return ReductionSettings.NumOfVertPercentage < Threshold_One; + } + break; + case SkeletalMeshTerminationCriterion::SMTC_TriangleOrVert: + { + return ReductionSettings.NumOfTrianglesPercentage < Threshold_One || ReductionSettings.NumOfVertPercentage < Threshold_One; + } + break; + } + + return false; + } + /** * Reduces the provided skeletal mesh. * @returns true if reduction was successful. diff --git a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierLinearAlgebra.h b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierLinearAlgebra.h index 12a8ab13100e..19835ec333b8 100644 --- a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierLinearAlgebra.h +++ b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierLinearAlgebra.h @@ -1546,11 +1546,7 @@ namespace SkeletalSimplifier /* default initialized puts the bbox in an invalid state*/ FAABBox2d() { - MinMax[0] = FLT_MAX; - MinMax[1] = FLT_MAX; - - MinMax[2] = -FLT_MAX; - MinMax[3] = -FLT_MAX; + Reset(); } FAABBox2d(const FAABBox2d& Other) @@ -1561,6 +1557,15 @@ namespace SkeletalSimplifier MinMax[3] = Other.MinMax[3]; } + /* Set to a default empty state */ + void Reset() + { + MinMax[0] = FLT_MAX; + MinMax[1] = FLT_MAX; + + MinMax[2] = -FLT_MAX; + MinMax[3] = -FLT_MAX; + } /** * Expand this BBox to include the Other * @param Other - another Axis Aligned bbox @@ -1624,6 +1629,21 @@ namespace SkeletalSimplifier Point.Y = FMath::Clamp(Point.Y, MinMax[1], MinMax[3]); } + /** + * Clamp values that exceed the bbox + * @Param Point - point to be clamped by a padded version of this bbox + * @Param Fraction - fraction of box width to use for padding. + */ + void ClampPoint(FVector2D& Point, const float Fraction) const + { + const float HalfFrac = Fraction * 0.5f; + const float XPad = HalfFrac * (MinMax[2] - MinMax[0]); + const float YPad = HalfFrac * (MinMax[3] - MinMax[1]); + + Point.X = FMath::Clamp(Point.X, MinMax[0] - XPad, MinMax[2] + XPad); + Point.Y = FMath::Clamp(Point.Y, MinMax[1] - YPad, MinMax[3] + YPad); + } + /** * Min corner the bbox */ diff --git a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierQuadrics.h b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierQuadrics.h index d75aecef2b1e..b6354f3cad93 100644 --- a/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierQuadrics.h +++ b/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifierQuadrics.h @@ -27,6 +27,12 @@ namespace SkeletalSimplifier FEdgeQuadric(const Vec3d& Vert0Pos, const Vec3d& Vert1Pos, const Vec3d& FaceNormal, const double EdgeWeight); + FEdgeQuadric(const FEdgeQuadric& Other) : + CMatrix(Other.CMatrix), + D0Vector(Other.D0Vector), + CScalar(Other.CScalar) + {} + double Evaluate(const Vec3d& Pos) const; @@ -828,8 +834,9 @@ namespace SkeletalSimplifier auto BasicAttrAccessor = Vert.GetBasicAttrAccessor(); ComputeAttrs(Gamma, B1Matrix, D1Vector, Pos, BasicWeights, BasicAttrAccessor); - // Clamp first UV channel. - UVBBox.ClampPoint(Vert.BasicAttributes.TexCoords[0]); + // Clamp first UV channel to a slightly padded version of the UV support. + const float PaddingFactor = 0.2f; + UVBBox.ClampPoint(Vert.BasicAttributes.TexCoords[0], PaddingFactor); // Update the Additional Attrs diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterface.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterface.h index 0fba8babef84..0aebd9215458 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterface.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterface.h @@ -268,7 +268,7 @@ public: #if WITH_EDITORONLY_DATA UPROPERTY(EditAnywhere, Transient, Category = "Curve") - bool ShowInCurveEditor; + bool ShowInCurveEditor; #endif enum diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterfaceSkeletalMesh.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterfaceSkeletalMesh.h index 40c9e3313766..e1b29dc57844 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterfaceSkeletalMesh.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraDataInterfaceSkeletalMesh.h @@ -325,6 +325,19 @@ public: UPROPERTY(EditAnywhere, Category = "Skeleton") TArray SpecificSockets; + + /** Whether any triangle sampling function is bound. Only used in game. */ + UPROPERTY() + bool bUseTriangleSampling; + + /** Whether any vertex sampling function is bound. Only used in game. */ + UPROPERTY() + bool bUseVertexSampling; + + /** Whether any skeleton sampling function is bound. Only used in game. */ + UPROPERTY() + bool bUseSkeletonSampling; + /** Cached change id off of the data interface.*/ uint32 ChangeId; diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraScript.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraScript.h index 66cab2a5c2c0..bd4aded804bc 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraScript.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Classes/NiagaraScript.h @@ -58,13 +58,13 @@ struct FNiagaraModuleDependency public: /** Specifies the provided id of the required dependent module (e.g. 'ProvidesNormalizedAge') */ UPROPERTY(AssetRegistrySearchable, EditAnywhere, Category = Script) - FName Id; + FName Id; /** Whether the dependency belongs before or after this module */ UPROPERTY(AssetRegistrySearchable, EditAnywhere, Category = Script) - ENiagaraModuleDependencyType Type; // e.g. PreDependency, + ENiagaraModuleDependencyType Type; // e.g. PreDependency, /** Detailed description of the dependency */ UPROPERTY(AssetRegistrySearchable, EditAnywhere, Category = Script, meta = (MultiLine = true)) - FText Description; + FText Description; }; struct FNiagaraScriptDebuggerInfo diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceCollisionQuery.cpp b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceCollisionQuery.cpp index 7e8927a1d720..75c1cd351dbc 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceCollisionQuery.cpp +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceCollisionQuery.cpp @@ -178,7 +178,7 @@ bool UNiagaraDataInterfaceCollisionQuery::GetFunctionHLSL(const FName& Definitio Out_CollisionNormal = WorldNormal;\n\ Out_Friction = 0.0f;\n\ Out_Restitution = 1.0f;\n\ - Out_QueryID = 0.0f;\ + Out_QueryID = 0;\ }\n\ else\n\ {\n\ diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceSkeletalMesh.cpp b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceSkeletalMesh.cpp index f7837f7de5a8..1bc8d4ff7632 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceSkeletalMesh.cpp +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraDataInterfaceSkeletalMesh.cpp @@ -432,7 +432,7 @@ bool FNDISkeletalMesh_InstanceData::Init(UNiagaraDataInterfaceSkeletalMesh* Inte LODIndex = FMath::Clamp(Interface->WholeMeshLOD, 0, Mesh->GetLODNum() - 1); } - if (!Mesh->GetLODInfo(LODIndex)->bAllowCPUAccess) + if (!Mesh->GetLODInfo(LODIndex)->bAllowCPUAccess && (GIsEditor || Interface->bUseTriangleSampling || Interface->bUseVertexSampling)) { UE_LOG(LogNiagara, Warning, TEXT("Skeletal Mesh Data Interface is trying to spawn from a whole mesh that does not allow CPU Access.\nInterface: %s\nMesh: %s\nLOD: %d"), *Interface->GetFullName(), @@ -512,10 +512,10 @@ bool FNDISkeletalMesh_InstanceData::Init(UNiagaraDataInterfaceSkeletalMesh* Inte bool bNeedDataImmediately = true; //Grab a handle to the skinning data if we have a component to skin. - ENDISkeletalMesh_SkinningMode SkinningMode = Interface->SkinningMode; + ENDISkeletalMesh_SkinningMode SkinningMode = (GIsEditor || Interface->bUseTriangleSampling || Interface->bUseVertexSampling) ? Interface->SkinningMode : ENDISkeletalMesh_SkinningMode::None; FSkeletalMeshSkinningDataUsage Usage( LODIndex, - SkinningMode == ENDISkeletalMesh_SkinningMode::SkinOnTheFly || SkinningMode == ENDISkeletalMesh_SkinningMode::PreSkin, + SkinningMode == ENDISkeletalMesh_SkinningMode::SkinOnTheFly || SkinningMode == ENDISkeletalMesh_SkinningMode::PreSkin || Interface->bUseSkeletonSampling, SkinningMode == ENDISkeletalMesh_SkinningMode::PreSkin, bNeedDataImmediately); @@ -732,6 +732,9 @@ UNiagaraDataInterfaceSkeletalMesh::UNiagaraDataInterfaceSkeletalMesh(FObjectInit , Source(nullptr) , SkinningMode(ENDISkeletalMesh_SkinningMode::SkinOnTheFly) , WholeMeshLOD(INDEX_NONE) + , bUseTriangleSampling(true) + , bUseVertexSampling(true) + , bUseSkeletonSampling(true) , ChangeId(0) { @@ -755,6 +758,15 @@ void UNiagaraDataInterfaceSkeletalMesh::PostInitProperties() void UNiagaraDataInterfaceSkeletalMesh::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); + + // If the change comes from an interaction (and not just a generic change) reset the usage flags. + // todo : this and the usage binding need to be done in the a precompilation parsing (or whever the script is compiled). + if (PropertyChangedEvent.Property) + { + bUseTriangleSampling = false; + bUseVertexSampling = false; + bUseSkeletonSampling = false; + } ChangeId++; } @@ -783,6 +795,13 @@ void UNiagaraDataInterfaceSkeletalMesh::GetVMExternalFunction(const FVMExternalF if (OutFunc.IsBound()) { +#if WITH_EDITOR + if (!bUseTriangleSampling) + { + bUseTriangleSampling = true; + MarkPackageDirty(); + } +#endif // WITH_EDITOR return; } @@ -790,10 +809,28 @@ void UNiagaraDataInterfaceSkeletalMesh::GetVMExternalFunction(const FVMExternalF if (OutFunc.IsBound()) { +#if WITH_EDITOR + if (!bUseVertexSampling) + { + bUseVertexSampling = true; + MarkPackageDirty(); + } +#endif // WITH_EDITOR return; } BindSkeletonSamplingFunction(BindingInfo, InstData, OutFunc); + +#if WITH_EDITOR + if (OutFunc.IsBound()) + { + if (!bUseSkeletonSampling) + { + bUseSkeletonSampling = true; + MarkPackageDirty(); + } + } +#endif // WITH_EDITOR } @@ -812,6 +849,9 @@ bool UNiagaraDataInterfaceSkeletalMesh::CopyToInternal(UNiagaraDataInterface* De OtherTyped->WholeMeshLOD = WholeMeshLOD; OtherTyped->SpecificBones = SpecificBones; OtherTyped->SpecificSockets = SpecificSockets; + OtherTyped->bUseTriangleSampling = bUseTriangleSampling; + OtherTyped->bUseVertexSampling = bUseVertexSampling; + OtherTyped->bUseSkeletonSampling = bUseSkeletonSampling; return true; } @@ -866,7 +906,7 @@ TArray UNiagaraDataInterfaceSkeletalMesh::GetErrors( bool bHasNoMeshAssignedError = false; // Collect Errors - if (DefaultMesh != nullptr) + if (DefaultMesh != nullptr && (GIsEditor || bUseTriangleSampling || bUseVertexSampling)) { for (auto info : DefaultMesh->GetLODInfoArray()) { diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraFunctionLibrary.cpp b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraFunctionLibrary.cpp index 289360356bc6..f6648a512994 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraFunctionLibrary.cpp +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraFunctionLibrary.cpp @@ -6,6 +6,7 @@ #include "Engine/Engine.h" #include "NiagaraComponent.h" #include "NiagaraSystem.h" +#include "ContentStreaming.h" #include "NiagaraWorldManager.h" @@ -15,6 +16,18 @@ UNiagaraFunctionLibrary::UNiagaraFunctionLibrary(const FObjectInitializer& Objec } +UNiagaraComponent* CreateNiagaraSystem(UNiagaraSystem* SystemTemplate, UWorld* World, AActor* Actor, bool bAutoDestroy, EPSCPoolMethod PoolingMethod) +{ + // todo : implement pooling method. + + UNiagaraComponent* NiagaraComponent = NewObject((Actor ? Actor : (UObject*)World)); + NiagaraComponent->SetAutoDestroy(bAutoDestroy); + NiagaraComponent->bAllowAnyoneToDestroyMe = true; + NiagaraComponent->SetAsset(SystemTemplate); + return NiagaraComponent; +} + + /** * Spawns a Niagara System at the specified world location/rotation * @return The spawned UNiagaraComponent @@ -27,13 +40,10 @@ UNiagaraComponent* UNiagaraFunctionLibrary::SpawnSystemAtLocation(UObject* World UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); if (World != nullptr) { - AActor* Actor = World->GetWorldSettings(); - PSC = NewObject((Actor ? Actor : (UObject*)World)); + PSC = CreateNiagaraSystem(SystemTemplate, World, World->GetWorldSettings(), bAutoDestroy, EPSCPoolMethod::None); #if WITH_EDITORONLY_DATA PSC->bWaitForCompilationOnActivate = true; #endif - PSC->SetAsset(SystemTemplate); - PSC->SetAutoDestroy(bAutoDestroy); PSC->bAutoActivate = false; PSC->RegisterComponentWithWorld(World); @@ -65,10 +75,7 @@ UNiagaraComponent* UNiagaraFunctionLibrary::SpawnSystemAttached(UNiagaraSystem* } else { - AActor* Actor = AttachToComponent->GetOwner(); - PSC = NewObject((Actor ? Actor : (UObject*)AttachToComponent->GetWorld())); - PSC->SetAsset(SystemTemplate); - PSC->SetAutoDestroy(bAutoDestroy); + PSC = CreateNiagaraSystem(SystemTemplate, AttachToComponent->GetWorld(), AttachToComponent->GetOwner(), bAutoDestroy, EPSCPoolMethod::None); PSC->RegisterComponentWithWorld(AttachToComponent->GetWorld()); PSC->AttachToComponent(AttachToComponent, FAttachmentTransformRules::KeepRelativeTransform, AttachPointName); @@ -86,6 +93,78 @@ UNiagaraComponent* UNiagaraFunctionLibrary::SpawnSystemAttached(UNiagaraSystem* return PSC; } +/** +* Spawns a Niagara System attached to a component +* @return The spawned UNiagaraComponent +*/ + +UNiagaraComponent* UNiagaraFunctionLibrary::SpawnSystemAttached( + UNiagaraSystem* SystemTemplate, + USceneComponent* AttachToComponent, + FName AttachPointName, + FVector Location, + FRotator Rotation, + FVector Scale, + EAttachLocation::Type LocationType, + bool bAutoDestroy, + EPSCPoolMethod PoolingMethod +) +{ + UNiagaraComponent* PSC = nullptr; + if (SystemTemplate) + { + if (!AttachToComponent) + { + UE_LOG(LogScript, Warning, TEXT("UGameplayStatics::SpawnNiagaraEmitterAttached: NULL AttachComponent specified!")); + } + else + { + UWorld* const World = AttachToComponent->GetWorld(); + if (World && !World->IsNetMode(NM_DedicatedServer)) + { + PSC = CreateNiagaraSystem(SystemTemplate, World, AttachToComponent->GetOwner(), bAutoDestroy, PoolingMethod); + if (PSC) + { + PSC->SetupAttachment(AttachToComponent, AttachPointName); + + if (LocationType == EAttachLocation::KeepWorldPosition) + { + const FTransform ParentToWorld = AttachToComponent->GetSocketTransform(AttachPointName); + const FTransform ComponentToWorld(Rotation, Location, Scale); + const FTransform RelativeTM = ComponentToWorld.GetRelativeTransform(ParentToWorld); + PSC->RelativeLocation = RelativeTM.GetLocation(); + PSC->RelativeRotation = RelativeTM.GetRotation().Rotator(); + PSC->RelativeScale3D = RelativeTM.GetScale3D(); + } + else + { + PSC->RelativeLocation = Location; + PSC->RelativeRotation = Rotation; + + if (LocationType == EAttachLocation::SnapToTarget) + { + // SnapToTarget indicates we "keep world scale", this indicates we we want the inverse of the parent-to-world scale + // to calculate world scale at Scale 1, and then apply the passed in Scale + const FTransform ParentToWorld = AttachToComponent->GetSocketTransform(AttachPointName); + PSC->RelativeScale3D = Scale * ParentToWorld.GetSafeScaleReciprocal(ParentToWorld.GetScale3D()); + } + else + { + PSC->RelativeScale3D = Scale; + } + } + + PSC->RegisterComponentWithWorld(World); + PSC->Activate(true); + + // Notify the texture streamer so that PSC gets managed as a dynamic component. + IStreamingManager::Get().NotifyPrimitiveUpdated(PSC); + } + } + } + } + return PSC; +} /** * Set a constant in an emitter of a Niagara System diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp index 6e275ea68f30..a7c9c7851085 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraModule.cpp @@ -139,14 +139,17 @@ void INiagaraModule::StartupModule() FWorldDelegates::OnWorldCleanup.AddRaw(this, &INiagaraModule::OnWorldCleanup); FWorldDelegates::OnPreWorldFinishDestroy.AddRaw(this, &INiagaraModule::OnPreWorldFinishDestroy); - FWorldDelegates::OnWorldPostActorTick.AddRaw(this, &INiagaraModule::TickWorld); #if WITH_EDITOR - // This is done so that the editor classes are available to load in the cooker on editor builds even though it doesn't load the editor directly. - // UMG does something similar for similar reasons. - // @TODO We should remove this once Niagara is fully a plug-in. - FModuleManager::Get().LoadModule(TEXT("NiagaraEditor")); + if (!GIsEditor) + { + // Loading uncooked data in a game environment, we still need to get some functionality from the NiagaraEditor module. + // This includes the ability to compile scripts and load WITH_EDITOR_ONLY data. + // Note that when loading with the Editor, the NiagaraEditor module is loaded based on the plugin description. + FModuleManager::Get().LoadModule(TEXT("NiagaraEditor")); + } #endif + FWorldDelegates::OnWorldPostActorTick.AddRaw(this, &INiagaraModule::TickWorld); CVarDetailLevel.AsVariable()->SetOnChangedCallback(FConsoleVariableDelegate::CreateRaw(this, &INiagaraModule::OnChangeDetailLevel)); OnChangeDetailLevel(CVarDetailLevel.AsVariable()); diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraScript.cpp b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraScript.cpp index f993f7dc41ae..4fb81db4e640 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraScript.cpp +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Private/NiagaraScript.cpp @@ -1028,7 +1028,7 @@ void UNiagaraScript::CacheResourceShadersForRendering(bool bRegenerateId, bool b if (Source) { FNiagaraShaderScript* ResourceToCache; - ERHIFeatureLevel::Type CacheFeatureLevel = ERHIFeatureLevel::SM5; + ERHIFeatureLevel::Type CacheFeatureLevel = GMaxRHIFeatureLevel; ScriptResource.SetScript(this, FeatureLevel, CachedScriptVMId.CompilerVersionID, CachedScriptVMId.BaseScriptID, CachedScriptVMId.ReferencedDependencyIds, GetName()); //if (ScriptResourcesByFeatureLevel[FeatureLevel]) diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraCommon.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraCommon.h index 809036008c25..15f74f78a049 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraCommon.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraCommon.h @@ -187,7 +187,7 @@ struct FNiagaraScriptDataUsageInfo /** If true, this script reads attribute data. */ UPROPERTY() - bool bReadsAttributeData; + bool bReadsAttributeData; }; diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraComponent.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraComponent.h index 61343c17236c..20854d5b834b 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraComponent.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraComponent.h @@ -107,7 +107,7 @@ public: * @see AutoAttachParent, AutoAttachSocketName, AutoAttachLocationType */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Attachment) - uint32 bAutoManageAttachment : 1; + uint32 bAutoManageAttachment : 1; virtual void Activate(bool bReset = false)override; diff --git a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraFunctionLibrary.h b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraFunctionLibrary.h index 5ca65ab88329..24ebc7a54df4 100644 --- a/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraFunctionLibrary.h +++ b/Engine/Plugins/FX/Niagara/Source/Niagara/Public/NiagaraFunctionLibrary.h @@ -6,6 +6,7 @@ #include "UObject/ObjectMacros.h" #include "Engine/EngineTypes.h" #include "Kismet/BlueprintFunctionLibrary.h" +#include "Particles/WorldPSCPool.h" #include "NiagaraFunctionLibrary.generated.h" class UNiagaraComponent; @@ -31,6 +32,8 @@ class NIAGARA_API UNiagaraFunctionLibrary : public UBlueprintFunctionLibrary UFUNCTION(BlueprintCallable, Category = Niagara, meta = (Keywords = "niagara System", UnsafeDuringActorConstruction = "true")) static UNiagaraComponent* SpawnSystemAttached(UNiagaraSystem* SystemTemplate, USceneComponent* AttachToComponent, FName AttachPointName, FVector Location, FRotator Rotation, EAttachLocation::Type LocationType, bool bAutoDestroy); + static UNiagaraComponent* SpawnSystemAttached(UNiagaraSystem* SystemTemplate, USceneComponent* AttachToComponent, FName AttachPointName, FVector Location, FRotator Rotation, FVector Scale, EAttachLocation::Type LocationType, bool bAutoDestroy, EPSCPoolMethod PoolingMethod); + //This is gonna be totally reworked // UFUNCTION(BlueprintCallable, Category = Niagara, meta = (Keywords = "niagara System", UnsafeDuringActorConstruction = "true")) // static void SetUpdateScriptConstant(UNiagaraComponent* Component, FName EmitterName, FName ConstantName, FVector Value); diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraCore/Private/NiagaraCustomVersion.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraCore/Private/NiagaraCustomVersion.cpp index 9578bf83b4a4..14110124d900 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraCore/Private/NiagaraCustomVersion.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraCore/Private/NiagaraCustomVersion.cpp @@ -8,4 +8,4 @@ const FGuid FNiagaraCustomVersion::GUID(0xFCF57AFA, 0x50764283, 0xB9A9E658, 0xFF // Register the custom version with core FCustomVersionRegistration GRegisterNiagaraCustomVersion(FNiagaraCustomVersion::GUID, FNiagaraCustomVersion::LatestVersion, TEXT("NiagaraVer")); -const FGuid FNiagaraCustomVersion::LatestScriptCompileVersion(0x044AB7CE, 0x10B040DD, 0x9C859C8B, 0xC0E47E09); \ No newline at end of file +const FGuid FNiagaraCustomVersion::LatestScriptCompileVersion(0x124E242E, 0xC5334EB6, 0xB88A6679, 0x582DE2EE); \ No newline at end of file diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraCurveOwner.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraCurveOwner.cpp index b4f1e377582b..18c9391471df 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraCurveOwner.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraCurveOwner.cpp @@ -86,7 +86,7 @@ void FNiagaraCurveOwner::OnCurveChanged(const TArray& Change UObject** CurveOwner = EditInfoToOwnerMap.Find(ChangedCurveEditInfo); if (CurveChanged != nullptr && CurveOwner != nullptr) { - CurveChanged->Execute(ChangedCurveEditInfo.CurveToEdit, *CurveOwner); + CurveChanged->Execute((FRichCurve*)ChangedCurveEditInfo.CurveToEdit, *CurveOwner); (*CurveOwner)->PostEditChange(); } } diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraEditorModule.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraEditorModule.cpp index 75abe566cf79..52c98f15ae45 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraEditorModule.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraEditor/Private/NiagaraEditorModule.cpp @@ -478,8 +478,11 @@ void FNiagaraEditorModule::StartupModule() TEXT("Dumps the values of the rapid iteration parameters for the specified asset by path."), FConsoleCommandWithArgsDelegate::CreateStatic(&DumpRapidIterationParamersForAsset)); - UThumbnailManager::Get().RegisterCustomRenderer(UNiagaraEmitter::StaticClass(), UNiagaraEmitterThumbnailRenderer::StaticClass()); - UThumbnailManager::Get().RegisterCustomRenderer(UNiagaraSystem::StaticClass(), UNiagaraSystemThumbnailRenderer::StaticClass()); + if (GIsEditor) + { + UThumbnailManager::Get().RegisterCustomRenderer(UNiagaraEmitter::StaticClass(), UNiagaraEmitterThumbnailRenderer::StaticClass()); + UThumbnailManager::Get().RegisterCustomRenderer(UNiagaraSystem::StaticClass(), UNiagaraSystemThumbnailRenderer::StaticClass()); + } } @@ -559,7 +562,7 @@ void FNiagaraEditorModule::ShutdownModule() IConsoleManager::Get().UnregisterConsoleObject(DumpRapidIterationParametersForAsset); } - if (UObjectInitialized()) + if (UObjectInitialized() && GIsEditor) { UThumbnailManager::Get().UnregisterCustomRenderer(UNiagaraEmitter::StaticClass()); UThumbnailManager::Get().UnregisterCustomRenderer(UNiagaraSystem::StaticClass()); diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraEditorWidgets/Private/DetailCustomizations/NiagaraDataInterfaceCurveDetails.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraEditorWidgets/Private/DetailCustomizations/NiagaraDataInterfaceCurveDetails.cpp index dcae6bb714b9..ee1ff2839d23 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraEditorWidgets/Private/DetailCustomizations/NiagaraDataInterfaceCurveDetails.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraEditorWidgets/Private/DetailCustomizations/NiagaraDataInterfaceCurveDetails.cpp @@ -229,10 +229,11 @@ public: for (const FRichCurveEditInfo& CurveEditInfo : CurveOwner->GetCurves()) { - if (CurveEditInfo.CurveToEdit->GetNumKeys()) + FRealCurve* Curve = CurveEditInfo.CurveToEdit; + if (Curve->GetNumKeys()) { - ViewMinInput = FMath::Min(ViewMinInput, CurveEditInfo.CurveToEdit->GetFirstKey().Time); - ViewMaxOutput = FMath::Max(ViewMaxOutput, CurveEditInfo.CurveToEdit->GetLastKey().Time); + ViewMinInput = FMath::Min(ViewMinInput, Curve->GetKeyTime(Curve->GetFirstKeyHandle())); + ViewMaxOutput = FMath::Max(ViewMaxOutput, Curve->GetKeyTime(Curve->GetLastKeyHandle())); } } diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Private/NiagaraShared.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Private/NiagaraShared.cpp index ea7d925e79dc..5b173b62e5d3 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Private/NiagaraShared.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Private/NiagaraShared.cpp @@ -448,6 +448,7 @@ bool FNiagaraShaderScript::BeginCompileShaderMap( { INiagaraShaderModule NiagaraShaderModule = FModuleManager::GetModuleChecked(TEXT("NiagaraShader")); NiagaraShaderModule.ProcessShaderCompilationQueue(); + OutShaderMap = NewShaderMap; } else { diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShader.h b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShader.h index 0afe53d8191b..383e3931a591 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShader.h +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShader.h @@ -48,7 +48,7 @@ public: static bool ShouldCompilePermutation(EShaderPlatform Platform, const FNiagaraShaderScript* Script) { //@todo - lit materials only - return IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5); + return RHISupportsComputeShaders(Platform); } diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShared.h b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShared.h index c44716f855f2..46f62019c2ec 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShared.h +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraShader/Public/NiagaraShared.h @@ -121,7 +121,7 @@ public: FNiagaraShaderMapId() : CompilerVersionID() - , FeatureLevel(ERHIFeatureLevel::SM5) + , FeatureLevel(GMaxRHIFeatureLevel) , BaseScriptID(0, 0, 0, 0) { } @@ -466,7 +466,7 @@ public: FNiagaraShaderScript() : GameThreadShaderMap(NULL), RenderingThreadShaderMap(NULL), - FeatureLevel(ERHIFeatureLevel::SM4), + FeatureLevel(GMaxRHIFeatureLevel), bLoadedCookedShaderMapId(false) {} diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraMeshVertexFactory.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraMeshVertexFactory.cpp index 82749b07993e..805521221788 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraMeshVertexFactory.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraMeshVertexFactory.cpp @@ -193,7 +193,7 @@ FShaderResourceViewRHIParamRef FNiagaraMeshVertexFactory::GetPreviousTransformBu bool FNiagaraMeshVertexFactory::ShouldCompilePermutation(EShaderPlatform Platform, const class FMaterial* Material, const class FShaderType* ShaderType) { - return (!IsMobilePlatform(Platform) && Platform != SP_OPENGL_SM4 && (Material->IsUsedWithNiagaraMeshParticles() || Material->IsSpecialEngineMaterial())); + return (IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5) || IsFeatureLevelSupported(Platform, ERHIFeatureLevel::ES3_1)) && (Material->IsUsedWithNiagaraMeshParticles() || Material->IsSpecialEngineMaterial()); } void FNiagaraMeshVertexFactory::SetData(const FStaticMeshDataType& InData) diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraRibbonVertexFactory.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraRibbonVertexFactory.cpp index 2d93686b5d82..d9b76765cb1d 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraRibbonVertexFactory.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraRibbonVertexFactory.cpp @@ -154,7 +154,7 @@ static TGlobalResource GNiagaraRibbonVertexDecl bool FNiagaraRibbonVertexFactory::ShouldCompilePermutation(EShaderPlatform Platform, const class FMaterial* Material, const class FShaderType* ShaderType) { - return (!IsMobilePlatform(Platform) && Platform != SP_OPENGL_SM4 && (Material->IsUsedWithNiagaraRibbons() || Material->IsSpecialEngineMaterial())); + return (IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5) || IsFeatureLevelSupported(Platform, ERHIFeatureLevel::ES3_1)) && (Material->IsUsedWithNiagaraRibbons() || Material->IsSpecialEngineMaterial()); } /** diff --git a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraSpriteVertexFactory.cpp b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraSpriteVertexFactory.cpp index bcf9790b099b..efb209a2d155 100644 --- a/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraSpriteVertexFactory.cpp +++ b/Engine/Plugins/FX/Niagara/Source/NiagaraVertexFactories/Private/NiagaraSpriteVertexFactory.cpp @@ -225,7 +225,7 @@ inline TGlobalResource& GetNiagaraSpriteVertexD bool FNiagaraSpriteVertexFactory::ShouldCompilePermutation(EShaderPlatform Platform, const class FMaterial* Material, const class FShaderType* ShaderType) { - return (!IsES2Platform(Platform) && Platform != SP_OPENGL_SM4 && (Material->IsUsedWithNiagaraSprites() || Material->IsSpecialEngineMaterial())); + return (IsFeatureLevelSupported(Platform, ERHIFeatureLevel::SM5) || IsFeatureLevelSupported(Platform, ERHIFeatureLevel::ES3_1)) && (Material->IsUsedWithNiagaraSprites() || Material->IsSpecialEngineMaterial()); } /** diff --git a/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeap/Classes/LightingTrackingComponent.h b/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeap/Classes/LightingTrackingComponent.h index d95ce6c45343..cd03c46201ae 100644 --- a/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeap/Classes/LightingTrackingComponent.h +++ b/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeap/Classes/LightingTrackingComponent.h @@ -60,10 +60,10 @@ public: /** Set to true if you want the global ambience value from the cameras to be used in post processing. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LightingTracking|MagicLeap") - bool UseGlobalAmbience; + bool UseGlobalAmbience; /** Set to true if you want the color temperature value from the cameras to be used in post processing. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LightingTracking|MagicLeap") - bool UseColorTemp; + bool UseColorTemp; /** Set to true if you want the ambient cube map to be dynamically updated from the cameras' data. */ //UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LightingTracking|MagicLeap") //bool UseDynamicAmbientCubeMap; diff --git a/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeapController/Public/MagicLeapControllerFunctionLibrary.h b/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeapController/Public/MagicLeapControllerFunctionLibrary.h index f1722f3d06ac..9c69bfaf0268 100644 --- a/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeapController/Public/MagicLeapControllerFunctionLibrary.h +++ b/Engine/Plugins/Lumin/MagicLeap/Source/MagicLeapController/Public/MagicLeapControllerFunctionLibrary.h @@ -91,12 +91,12 @@ public: @return True if the command to set the tracking mode was successfully sent to the controller, false otherwise. */ UFUNCTION(BlueprintCallable, Category = "MotionController|MagicLeap") - static bool SetControllerTrackingMode(EMLControllerTrackingMode TrackingMode); + static bool SetControllerTrackingMode(EMLControllerTrackingMode TrackingMode); /** Get controller tracking mode. @return Controller tracking mode. */ UFUNCTION(BlueprintCallable, Category = "MotionController|MagicLeap") - static EMLControllerTrackingMode GetControllerTrackingMode(); + static EMLControllerTrackingMode GetControllerTrackingMode(); }; diff --git a/Engine/Plugins/Media/AndroidMedia/Source/AndroidMedia/Private/Player/AndroidMediaPlayer.cpp b/Engine/Plugins/Media/AndroidMedia/Source/AndroidMedia/Private/Player/AndroidMediaPlayer.cpp index 99836a285463..f04667f88c3a 100644 --- a/Engine/Plugins/Media/AndroidMedia/Source/AndroidMedia/Private/Player/AndroidMediaPlayer.cpp +++ b/Engine/Plugins/Media/AndroidMedia/Source/AndroidMedia/Private/Player/AndroidMediaPlayer.cpp @@ -16,6 +16,9 @@ #include "UObject/Class.h" #include "UObject/UObjectGlobals.h" #include "ExternalTexture.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformFilemanager.h" +#include "IPlatformFilePak.h" #include "Android/AndroidJavaMediaPlayer.h" #include "AndroidMediaTextureSample.h" @@ -260,31 +263,79 @@ bool FAndroidMediaPlayer::Open(const FString& Url, const IMediaOptions* /*Option // make sure that file exists if (!PlatformFile.FileExists(*FilePath)) { - UE_LOG(LogAndroidMedia, Warning, TEXT("File doesn't exist %s."), *FilePath); + // possible it is in a PAK file + FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName())); - return false; - } - - // get information about media - int64 FileOffset = PlatformFile.FileStartOffset(*FilePath); - int64 FileSize = PlatformFile.FileSize(*FilePath); - FString FileRootPath = PlatformFile.FileRootPath(*FilePath); - - // play movie as a file or asset - if (PlatformFile.IsAsset(*FilePath)) - { - if (!JavaMediaPlayer->SetDataSource(PlatformFile.GetAssetManager(), FileRootPath, FileOffset, FileSize)) + FPakFile* PakFile = NULL; + FPakEntry FileEntry; + if (!PakPlatformFile->FindFileInPakFiles(*FilePath, &PakFile, &FileEntry)) { - UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for asset %s"), *FilePath); + UE_LOG(LogAndroidMedia, Warning, TEXT("File doesn't exist %s."), *FilePath); return false; } + + // is it a simple case (can just use file datasource)? + if (FileEntry.CompressionMethod == COMPRESS_None && !FileEntry.IsEncrypted()) + { + FString PakFilename = PakFile->GetFilename(); + int64 PakHeaderSize = FileEntry.GetSerializedSize(PakFile->GetInfo().Version); + int64 OffsetInPak = FileEntry.Offset + PakHeaderSize; + int64 FileSize = FileEntry.Size; + + //UE_LOG(LogAndroidMedia, Warning, TEXT("not compressed or encrypted PAK: Filename for PAK is: %s"), *PakFilename); + //UE_LOG(LogAndroidMedia, Warning, TEXT("PAK Offset: %lld, Size: %lld (header=%lld)"), OffsetInPak, FileSize, PakHeaderSize); + + int64 FileOffset = PlatformFile.FileStartOffset(*PakFilename) + OffsetInPak; + FString FileRootPath = PlatformFile.FileRootPath(*PakFilename); + + //UE_LOG(LogAndroidMedia, Warning, TEXT("Final Offset: %lld in %s"), FileOffset, *FileRootPath); + + if (!JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize)) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for file in PAK %s"), *FilePath); + return false; + } + } + else + { + // only support media data source on Android 6.0+ + if (FAndroidMisc::GetAndroidBuildVersion() < 23) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("File cannot be played on this version of Android due to PAK file with compression or encryption: %s."), *FilePath); + return false; + } + + TSharedRef Archive = MakeShareable(IFileManager::Get().CreateFileReader(*FilePath)); + if (!JavaMediaPlayer->SetDataSource(Archive)) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for archive %s"), *FilePath); + return false; + } + } } else { - if (!JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize)) + // get information about media + int64 FileOffset = PlatformFile.FileStartOffset(*FilePath); + int64 FileSize = PlatformFile.FileSize(*FilePath); + FString FileRootPath = PlatformFile.FileRootPath(*FilePath); + + // play movie as a file or asset + if (PlatformFile.IsAsset(*FilePath)) { - UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for file %s"), *FilePath); - return false; + if (!JavaMediaPlayer->SetDataSource(PlatformFile.GetAssetManager(), FileRootPath, FileOffset, FileSize)) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for asset %s"), *FilePath); + return false; + } + } + else + { + if (!JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize)) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for file %s"), *FilePath); + return false; + } } } } @@ -318,9 +369,65 @@ bool FAndroidMediaPlayer::Open(const FString& Url, const IMediaOptions* /*Option } -bool FAndroidMediaPlayer::Open(const TSharedRef& /*Archive*/, const FString& /*OriginalUrl*/, const IMediaOptions* /*Options*/) +bool FAndroidMediaPlayer::Open(const TSharedRef& Archive, const FString& OriginalUrl, const IMediaOptions* /*Options*/) { - return false; // @todo AndroidMedia: implement opening media from FArchive +#if ANDROIDMEDIAPLAYER_USE_NATIVELOGGING + FPlatformMisc::LowLevelOutputDebugStringf(TEXT("FAndroidMedia::OpenArchive(%s) - %s"), *OriginalUrl, *PlayerGuid.ToString()); +#endif + + if (CurrentState == EMediaState::Error) + { + return false; + } + + if (Archive->TotalSize() == 0) + { + UE_LOG(LogAndroidMedia, Verbose, TEXT("Player %p: Cannot open media from archive (archive is empty)"), this); + return false; + } + + if (OriginalUrl.IsEmpty()) + { + UE_LOG(LogAndroidMedia, Verbose, TEXT("Player %p: Cannot open media from archive (no original URL provided)"), this); + return false; + } + + Close(); + + // only support media data source on Android 6.0+ + if (FAndroidMisc::GetAndroidBuildVersion() < 23) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("File cannot be played on this version of Android due to PAK file with compression or encryption: %s."), *OriginalUrl); + return false; + } + + if (!JavaMediaPlayer->SetDataSource(Archive)) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for archive %s"), *OriginalUrl); + return false; + } + + // prepare media + MediaUrl = OriginalUrl; + +#if ANDROIDMEDIAPLAYER_USE_PREPAREASYNC + if (!JavaMediaPlayer->PrepareAsync()) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to prepare media source %s"), *OriginalUrl); + return false; + } + + CurrentState = EMediaState::Preparing; + return true; +#else + if (!JavaMediaPlayer->Prepare()) + { + UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to prepare media source %s"), *OriginalUrl); + return false; + } + + return InitializePlayer(); +#endif } diff --git a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.cpp b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.cpp index 163220c33034..9c45803db2c5 100644 --- a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.cpp +++ b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.cpp @@ -19,6 +19,7 @@ #include "AvfMediaTracks.h" #include "AvfMediaUtils.h" +#include "IMediaAudioSample.h" /* FAVPlayerDelegate @@ -93,6 +94,52 @@ @end +/* Sync Control Class for consumed samples */ +class FAvfMediaSamples : public FMediaSamples +{ +public: + FAvfMediaSamples() + : FMediaSamples() + , AudioSyncSampleTime(FTimespan::MinValue()) + , VideoSyncSampleTime(FTimespan::MinValue()) + {} + + virtual ~FAvfMediaSamples() + {} + + virtual bool FetchAudio(TRange TimeRange, TSharedPtr& OutSample) override + { + bool bResult = FMediaSamples::FetchAudio(TimeRange, OutSample); + + if(FTimespan::MinValue() == AudioSyncSampleTime && bResult && OutSample.IsValid()) + { + AudioSyncSampleTime = OutSample->GetTime() + OutSample->GetDuration(); + } + + return bResult; + } + + virtual bool FetchVideo(TRange TimeRange, TSharedPtr& OutSample) + { + bool bResult = FMediaSamples::FetchVideo(TimeRange, OutSample); + + if(FTimespan::MinValue() == VideoSyncSampleTime && bResult && OutSample.IsValid()) + { + VideoSyncSampleTime = OutSample->GetTime() + OutSample->GetDuration(); + } + + return bResult; + } + + void ClearSyncSampleTimes () { AudioSyncSampleTime = VideoSyncSampleTime = FTimespan::MinValue(); } + FTimespan GetAudioSyncSampleTime() const { return AudioSyncSampleTime; } + FTimespan GetVideoSyncSampleTime() const { return VideoSyncSampleTime; } + +private: + + TAtomic AudioSyncSampleTime; + TAtomic VideoSyncSampleTime; +}; /* FAvfMediaPlayer structors *****************************************************************************/ @@ -113,8 +160,10 @@ FAvfMediaPlayer::FAvfMediaPlayer(IMediaEventSink& InEventSink) PlayerItem = nil; bPrerolled = false; + bTimeSynced = false; + bSeeking = false; - Samples = new FMediaSamples; + Samples = new FAvfMediaSamples; Tracks = new FAvfMediaTracks(*Samples); } @@ -127,6 +176,37 @@ FAvfMediaPlayer::~FAvfMediaPlayer() Samples = nullptr; } +/* FAvfMediaPlayer Sample Sync helpers + *****************************************************************************/ +void FAvfMediaPlayer::ClearTimeSync() +{ + bTimeSynced = false; + if(Samples != nullptr) + { + Samples->ClearSyncSampleTimes(); + } +} + +FTimespan FAvfMediaPlayer::GetAudioTimeSync() const +{ + FTimespan Sync = FTimespan::MinValue(); + if(Samples != nullptr) + { + Sync = Samples->GetAudioSyncSampleTime(); + } + return Sync; +} + +FTimespan FAvfMediaPlayer::GetVideoTimeSync() const +{ + FTimespan Sync = FTimespan::MinValue(); + if(Samples != nullptr) + { + Sync = Samples->GetVideoSyncSampleTime(); + } + return Sync; +} + /* FAvfMediaPlayer interface *****************************************************************************/ @@ -228,7 +308,9 @@ void FAvfMediaPlayer::OnStatusNotification() } case AVPlayerItemStatusUnknown: default: + { break; + } } }); } @@ -268,7 +350,7 @@ void FAvfMediaPlayer::Close() WillDeactivateHandle.Reset(); } - CurrentTime = 0; + CurrentTime = FTimespan::Zero(); MediaUrl = FString(); if (PlayerItem != nil) @@ -307,8 +389,11 @@ void FAvfMediaPlayer::Close() EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed); bPrerolled = false; + bSeeking = false; CurrentRate = 0.f; + + ClearTimeSync(); } @@ -525,14 +610,62 @@ void FAvfMediaPlayer::TickFetch(FTimespan DeltaTime, FTimespan /*Timecode*/) void FAvfMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan /*Timecode*/) { - // Prevent deadlock - can't do this in TickAudio if ((CurrentState > EMediaState::Error) && (Duration > FTimespan::Zero())) { - if(MediaPlayer != nil) + switch(CurrentState) { - CMTime Current = [MediaPlayer currentTime]; - FTimespan DiplayTime = FTimespan::FromSeconds(CMTimeGetSeconds(Current)); - CurrentTime = FMath::Min(DiplayTime, Duration); + case EMediaState::Playing: + { + if(bSeeking) + { + ClearTimeSync(); + } + else + { + if(!bTimeSynced) + { + FTimespan SyncTime = FTimespan::MinValue(); +#if AUDIO_PLAYBACK_VIA_ENGINE + if(Tracks->GetSelectedTrack(EMediaTrackType::Audio) != INDEX_NONE) + { + SyncTime = GetAudioTimeSync(); + } + else if(Tracks->GetSelectedTrack(EMediaTrackType::Video) != INDEX_NONE) + { + SyncTime = GetVideoTimeSync(); + } + else /* Default Use AVPlayer time*/ +#endif + { + SyncTime = FTimespan::FromSeconds(CMTimeGetSeconds(MediaPlayer.currentTime)); + } + + if(SyncTime != FTimespan::MinValue()) + { + bTimeSynced = true; + CurrentTime = SyncTime; + } + } + else + { + CurrentTime += DeltaTime * CurrentRate; + } + } + break; + } + case EMediaState::Stopped: + case EMediaState::Closed: + case EMediaState::Error: + case EMediaState::Preparing: + { + CurrentTime = FTimespan::Zero(); + break; + } + case EMediaState::Paused: + default: + { + break; + } } } @@ -621,13 +754,15 @@ bool FAvfMediaPlayer::IsLooping() const return ShouldLoop; } - bool FAvfMediaPlayer::Seek(const FTimespan& Time) { - CurrentTime = Time; - if (bPrerolled) { + bSeeking = true; + ClearTimeSync(); + + CurrentTime = Time; + double TotalSeconds = Time.GetTotalSeconds(); CMTime CurrentTimeInSeconds = CMTimeMakeWithSeconds(TotalSeconds, 1000); @@ -638,6 +773,7 @@ bool FAvfMediaPlayer::Seek(const FTimespan& Time) { PlayerTasks.Enqueue([=]() { + bSeeking = false; EventSink.ReceiveMediaEvent(EMediaEvent::SeekCompleted); }); } @@ -673,16 +809,37 @@ bool FAvfMediaPlayer::SetRate(float Rate) { [MediaPlayer setRate : CurrentRate]; - if (FMath::IsNearlyZero(Rate)) + if (FMath::IsNearlyZero(CurrentRate) && CurrentState != EMediaState::Paused) { CurrentState = EMediaState::Paused; EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended); } else { - CurrentState = EMediaState::Playing; - EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed); + if(CurrentState != EMediaState::Playing) + { + ClearTimeSync(); + + CurrentState = EMediaState::Playing; + EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed); + } } + + // Use AVPlayer Mute to control reverse playback audio playback + // Only needed if !AUDIO_PLAYBACK_VIA_ENGINE - however - keep all platforms the same + bool bMuteAudio = Rate < 0.f; + if(bMuteAudio) + { + MediaPlayer.muted = YES; + } + else + { + MediaPlayer.muted = NO; + } + +#if AUDIO_PLAYBACK_VIA_ENGINE + Tracks->ApplyMuteState(bMuteAudio); +#endif } return true; diff --git a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.h b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.h index dbffca08f598..262f7b485240 100644 --- a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.h +++ b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaPlayer.h @@ -11,7 +11,7 @@ #import class FAvfMediaTracks; -class FMediaSamples; +class FAvfMediaSamples; class IMediaEventSink; @class AVPlayer; @@ -99,7 +99,14 @@ private: /** Callback for when the application is moved from the active to inactive state */ void HandleApplicationDeactivate(); - + + /** Clears the Time Sync flag*/ + void ClearTimeSync(); + + /** Returns the consumed buffer type sync Points */ + FTimespan GetAudioTimeSync() const; + FTimespan GetVideoTimeSync() const; + /** The current playback rate. */ float CurrentRate; @@ -134,7 +141,7 @@ private: TQueue> PlayerTasks; /** The media sample queue. */ - FMediaSamples* Samples; + FAvfMediaSamples* Samples; /** Should the video loop to the beginning at completion */ bool ShouldLoop; @@ -144,6 +151,12 @@ private: /** Playback primed and ready when set */ bool bPrerolled; + + /** Media Player is currently seeking */ + bool bSeeking; + + /** Set false until the first audio (or video if none) sample has been consumed after seeking or prerolling or, on non Engine mixer platforms first tick after seek */ + bool bTimeSynced; /** Mutex to ensure thread-safe access */ FCriticalSection CriticalSection; diff --git a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.cpp b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.cpp index 74fc06930bfc..1febe5731365 100644 --- a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.cpp +++ b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.cpp @@ -18,8 +18,6 @@ #import -#define AUDIO_PLAYBACK_VIA_ENGINE (PLATFORM_MAC) - NS_ASSUME_NONNULL_BEGIN /* FAVPlayerItemLegibleOutputPushDelegate @@ -66,12 +64,14 @@ struct AudioTrackTapContextData AudioStreamBasicDescription DestinationFormat; FMediaSamples& SampleQueue; FAvfMediaAudioSamplePool* AudioSamplePool; - bool bActive; + bool bActive; // Init and shutdown flag + volatile bool& bMuted; // Muted usually with -ve playback rate - AudioTrackTapContextData(FMediaSamples& InSampleQueue, FAvfMediaAudioSamplePool* InAudioSamplePool, AudioStreamBasicDescription const & InDestinationFormat) + AudioTrackTapContextData(FMediaSamples& InSampleQueue, FAvfMediaAudioSamplePool* InAudioSamplePool, AudioStreamBasicDescription const & InDestinationFormat, volatile bool& bInBindMuted) : SampleQueue(InSampleQueue) , AudioSamplePool(InAudioSamplePool) , bActive(false) + , bMuted(bInBindMuted) { FMemory::Memcpy(&DestinationFormat, &InDestinationFormat, sizeof(AudioStreamBasicDescription)); } @@ -115,7 +115,7 @@ static void AudioTrackTapProcess(MTAudioProcessingTapRef __nonnull TapRef, // in the public interface to AvfMediaTracks which seems wrong - plus we save the extra function call in time critical code! check(Ctx); - if(Ctx->bActive) + if(Ctx->bActive && !Ctx->bMuted) { // Compute required buffer size uint32 BufferSize = (NumberFrames * ((Ctx->DestinationFormat.mBitsPerChannel / 8))) * Ctx->DestinationFormat.mChannelsPerFrame; @@ -125,19 +125,19 @@ static void AudioTrackTapProcess(MTAudioProcessingTapRef __nonnull TapRef, FTimespan Duration(((int64)NumberFrames * ETimespan::TicksPerSecond) / (int64)Ctx->DestinationFormat.mSampleRate); // If valid set time stamps give by the system - if((TimeRange.start.flags & kCMTimeFlags_Valid) != 0) + if((TimeRange.start.flags & kCMTimeFlags_Valid) == kCMTimeFlags_Valid) { StartTime = (TimeRange.start.value * ETimespan::TicksPerSecond) / TimeRange.start.timescale; } - + // On pause the duration from system can be different from computed - if((TimeRange.duration.flags & kCMTimeFlags_Valid) != 0) + if((TimeRange.duration.flags & kCMTimeFlags_Valid) == kCMTimeFlags_Valid) { Duration = (TimeRange.duration.value * ETimespan::TicksPerSecond) / TimeRange.duration.timescale; } // Don't add zero duration sample buffers to to the sink - if(Duration.GetTicks() != 0) + if(Duration.GetTicks() > 0) { // Get a media audio sample buffer from the pool const TSharedRef AudioSample = Ctx->AudioSamplePool->AcquireShared(); @@ -193,6 +193,16 @@ static void AudioTrackTapProcess(MTAudioProcessingTapRef __nonnull TapRef, } } } + else + { + // On mute or inactive make sure no audio 'leaks' through to the OS mixer - we can't rely on the outer AVPlayer when muted with this tap attached to be fast or clean about dealing with this + const uint32 BufferCount = BufferListInOut->mNumberBuffers; + for(uint32 b = 0;b < BufferCount;++b) + { + AudioBuffer& Buffer = BufferListInOut->mBuffers[b]; + FMemory::Memset(Buffer.mData, 0, Buffer.mDataByteSize); + } + } } } @@ -233,7 +243,7 @@ static void AudioTrackTapShutdownCurrentAudioTrackProcessing(AVPlayerItem* Playe } } -static void AudioTrackTapInitializeForAudioTrack(FMediaSamples& InSampleQueue, FAvfMediaAudioSamplePool* InAudioSamplePool, AudioStreamBasicDescription const & InDestinationFormat, AVPlayerItem* PlayerItem, AVAssetTrack* AssetTrack) +static void AudioTrackTapInitializeForAudioTrack(FMediaSamples& InSampleQueue, FAvfMediaAudioSamplePool* InAudioSamplePool, AudioStreamBasicDescription const & InDestinationFormat, AVPlayerItem* PlayerItem, AVAssetTrack* AssetTrack, volatile bool& bInBindMuted) { SCOPED_AUTORELEASE_POOL; @@ -247,7 +257,7 @@ static void AudioTrackTapInitializeForAudioTrack(FMediaSamples& InSampleQueue, F MTAudioProcessingTapCallbacks Callbacks; Callbacks.version = kMTAudioProcessingTapCallbacksVersion_0; - Callbacks.clientInfo = new AudioTrackTapContextData(InSampleQueue, InAudioSamplePool, InDestinationFormat); + Callbacks.clientInfo = new AudioTrackTapContextData(InSampleQueue, InAudioSamplePool, InDestinationFormat, bInBindMuted); Callbacks.init = AudioTrackTapInit; Callbacks.prepare = AudioTrackTapPrepare; Callbacks.process = AudioTrackTapProcess; @@ -642,8 +652,19 @@ void FAvfMediaTracks::Reset() } PlayerItem = nil; + +#if AUDIO_PLAYBACK_VIA_ENGINE + bMuted = false; +#endif } +#if AUDIO_PLAYBACK_VIA_ENGINE +void FAvfMediaTracks::ApplyMuteState(bool bMute) +{ + bMuted = bMute; +} +#endif + /* IMediaTracks interface *****************************************************************************/ @@ -889,7 +910,7 @@ bool FAvfMediaTracks::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex) TargetDesc.mBitsPerChannel = 32; TargetDesc.mReserved = 0; - AudioTrackTapInitializeForAudioTrack(Samples, AudioSamplePool, TargetDesc, PlayerItem, SelectedTrack.AssetTrack); + AudioTrackTapInitializeForAudioTrack(Samples, AudioSamplePool, TargetDesc, PlayerItem, SelectedTrack.AssetTrack, bMuted); #endif AVPlayerItemTrack* PlayerTrack = (AVPlayerItemTrack*)SelectedTrack.Output; diff --git a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.h b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.h index 143d35c7e9a2..0b14243b1ae7 100644 --- a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.h +++ b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Player/AvfMediaTracks.h @@ -12,6 +12,8 @@ #import +#define AUDIO_PLAYBACK_VIA_ENGINE (PLATFORM_MAC) + class FAvfMediaAudioSamplePool; class FAvfMediaVideoSamplePool; class FAvfMediaVideoSampler; @@ -99,6 +101,16 @@ public: /** Reset the stream collection. */ void Reset(); + +#if AUDIO_PLAYBACK_VIA_ENGINE + /** + * Allow independant audio mute when producing audio buffers for play back through the engine + * Muting will stop sending audio buffers to the media audio sink + * e.g. Gives the option to have fast mute on reverse otherwise we can get a few bad buffers + */ + void ApplyMuteState(bool bMute); +#endif + public: //~ IMediaTracks interface @@ -136,6 +148,11 @@ private: /** The player item containing the track information. */ AVPlayerItem* PlayerItem; + +#if AUDIO_PLAYBACK_VIA_ENGINE + /** Current Mute State. */ + volatile bool bMuted; +#endif /** The media sample queue. */ FMediaSamples& Samples; diff --git a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Shared/AvfMediaUtils.cpp b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Shared/AvfMediaUtils.cpp index a830362c8983..5a3355404a69 100644 --- a/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Shared/AvfMediaUtils.cpp +++ b/Engine/Plugins/Media/AvfMedia/Source/AvfMedia/Private/Shared/AvfMediaUtils.cpp @@ -99,7 +99,11 @@ namespace AvfMedia if (bForWrite) { +#if FILESHARING_ENABLED + static FString WritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/"); +#else static FString WritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/"); +#endif return WritePathBase + Result; } else diff --git a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneFolderExtensions.cpp b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneFolderExtensions.cpp new file mode 100644 index 000000000000..e60a873af06d --- /dev/null +++ b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneFolderExtensions.cpp @@ -0,0 +1,160 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "ExtensionLibraries/MovieSceneFolderExtensions.h" +#include "MovieSceneFolder.h" +#include "MovieSceneSequence.h" + + +FName UMovieSceneFolderExtensions::GetFolderName(UMovieSceneFolder* Folder) +{ + if (Folder) + { + return Folder->GetFolderName(); + } + + return FName(); +} + +bool UMovieSceneFolderExtensions::SetFolderName(UMovieSceneFolder* Folder, FName InFolderName) +{ + if (Folder) + { + Folder->SetFolderName(InFolderName); + return true; + } + + return false; +} + +FColor UMovieSceneFolderExtensions::GetFolderColor(UMovieSceneFolder* Folder) +{ +#if WITH_EDITORONLY_DATA + if (Folder) + { + return Folder->GetFolderColor(); + } +#endif //WITH_EDITORONLY_DATA + return FColor(); +} + +bool UMovieSceneFolderExtensions::SetFolderColor(UMovieSceneFolder* Folder, FColor InFolderColor) +{ +#if WITH_EDITORONLY_DATA + if (Folder) + { + Folder->SetFolderColor(InFolderColor); + return true; + } +#endif //WITH_EDITORONLY_DATA + + return false; +} + +TArray UMovieSceneFolderExtensions::GetChildFolders(UMovieSceneFolder* Folder) +{ + TArray Result; + + if (Folder) + { + Result = Folder->GetChildFolders(); + } + + return Result; +} + +bool UMovieSceneFolderExtensions::AddChildFolder(UMovieSceneFolder* TargetFolder, UMovieSceneFolder* FolderToAdd) +{ + if (TargetFolder && FolderToAdd) + { + TargetFolder->AddChildFolder(FolderToAdd); + return true; + } + + return false; +} + +bool UMovieSceneFolderExtensions::RemoveChildFolder(UMovieSceneFolder* TargetFolder, UMovieSceneFolder* FolderToRemove) +{ + if (TargetFolder && FolderToRemove) + { + TargetFolder->RemoveChildFolder(FolderToRemove); + return true; + } + + return false; +} + +TArray UMovieSceneFolderExtensions::GetChildMasterTracks(UMovieSceneFolder* Folder) +{ + TArray Result; + + if (Folder) + { + Result = Folder->GetChildMasterTracks(); + } + + return Result; +} + +bool UMovieSceneFolderExtensions::AddChildMasterTrack(UMovieSceneFolder* Folder, UMovieSceneTrack* InMasterTrack) +{ + if (Folder && InMasterTrack) + { + Folder->AddChildMasterTrack(InMasterTrack); + return true; + } + + return false; +} + +bool UMovieSceneFolderExtensions::RemoveChildMasterTrack(UMovieSceneFolder* Folder, UMovieSceneTrack* InMasterTrack) +{ + if (Folder && InMasterTrack) + { + Folder->RemoveChildMasterTrack(InMasterTrack); + return true; + } + + return false; +} + +TArray UMovieSceneFolderExtensions::GetChildObjectBindings(UMovieSceneFolder* Folder) +{ + TArray Result; + + if (Folder) + { + // Attempt to get the sequence reference from the folder + UMovieScene* MovieScene = Cast(Folder->GetOuter()); + UMovieSceneSequence* Sequence = Cast(MovieScene->GetOuter()); + + for (FGuid ID : Folder->GetChildObjectBindings()) + { + Result.Add(FSequencerBindingProxy(ID, Sequence)); + } + } + + return Result; +} + +bool UMovieSceneFolderExtensions::AddChildObjectBinding(UMovieSceneFolder* Folder, FSequencerBindingProxy InObjectBinding) +{ + if (Folder && InObjectBinding.BindingID.IsValid()) + { + Folder->AddChildObjectBinding(InObjectBinding.BindingID); + return true; + } + + return false; +} + +bool UMovieSceneFolderExtensions::RemoveChildObjectBinding(UMovieSceneFolder* Folder, const FSequencerBindingProxy InObjectBinding) +{ + if (Folder && InObjectBinding.BindingID.IsValid()) + { + Folder->RemoveChildObjectBinding(InObjectBinding.BindingID); + return true; + } + + return false; +} diff --git a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneSequenceExtensions.cpp b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneSequenceExtensions.cpp index 057dde25d3a5..3dba1eeef750 100644 --- a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneSequenceExtensions.cpp +++ b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Private/ExtensionLibraries/MovieSceneSequenceExtensions.cpp @@ -3,6 +3,7 @@ #include "ExtensionLibraries/MovieSceneSequenceExtensions.h" #include "MovieSceneSequence.h" #include "MovieScene.h" +#include "MovieSceneFolder.h" #include "Algo/Find.h" TArray UMovieSceneSequenceExtensions::FilterTracks(TArrayView InTracks, UClass* DesiredClass, bool bExactMatch) @@ -272,3 +273,37 @@ TArray UMovieSceneSequenceExtensions::LocateBoundObjects(UMovieSceneSe return Result; } +TArray UMovieSceneSequenceExtensions::GetRootFoldersInSequence(UMovieSceneSequence* Sequence) +{ + TArray Result; + + if (Sequence) + { + UMovieScene* Scene = Sequence->GetMovieScene(); + if (Scene) + { + Result = Scene->GetRootFolders(); + } + } + + return Result; +} + +UMovieSceneFolder* UMovieSceneSequenceExtensions::AddRootFolderToSequence(UMovieSceneSequence* Sequence, FString NewFolderName) +{ + UMovieSceneFolder* NewFolder = nullptr; + + if (Sequence) + { + UMovieScene* MovieScene = Sequence->GetMovieScene(); + if (MovieScene) + { + NewFolder = NewObject(MovieScene); + NewFolder->SetFolderName(FName(*NewFolderName)); + MovieScene->GetRootFolders().Add(NewFolder); + } + } + + return NewFolder; +} + diff --git a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneFolderExtensions.h b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneFolderExtensions.h new file mode 100644 index 000000000000..abe30c987278 --- /dev/null +++ b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneFolderExtensions.h @@ -0,0 +1,148 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "UObject/ObjectMacros.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "SequencerBindingProxy.h" + +#include "MovieSceneFolderExtensions.generated.h" + +class UMovieSceneFolder; +class UMovieSceneTrack; + +/** + * Function library containing methods that should be hoisted onto UMovieSceneFolders for scripting + */ +UCLASS() +class UMovieSceneFolderExtensions : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** + * Get the given folder's display name + * + * @param Folder The folder to use + * @return The target folder's name + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static FName GetFolderName(UMovieSceneFolder* Folder); + + /** + * Set the name of the given folder + * + * @param Folder The folder to set the name of + * @param InFolderName The new name for the folder + * @return True if the setting of the folder name succeeds + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool SetFolderName(UMovieSceneFolder* Folder, FName InFolderName); + + /** + * Get the display color of the given folder + * + * @param Folder The folder to get the display color of + * @return The display color of the given folder + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Editor Scripting|Sequencer Tools|Folders", meta = (ScriptMethod)) + static FColor GetFolderColor(UMovieSceneFolder* Folder); + + /** + * Set the display color of the given folder + * + * @param Folder The folder to set the display color of + * @param InFolderColor The new display color for the folder + * @return True if the folder's display color is set successfully + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool SetFolderColor(UMovieSceneFolder* Folder, FColor InFolderColor); + + /** + * Get the child folders of a given folder + * + * @param Folder The folder to get the child folders of + * @return The child folders associated with the given folder + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static TArray GetChildFolders(UMovieSceneFolder* Folder); + + /** + * Add a child folder to the target folder + * + * @param TargetFolder The folder to add a child folder to + * @param FolderToAdd The child folder to be added + * @return True if the addition is successful + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool AddChildFolder(UMovieSceneFolder* TargetFolder, UMovieSceneFolder* FolderToAdd); + + /** + * Remove a child folder from the given folder + * + * @param TargetFolder The folder from which to remove a child folder + * @param FolderToRemove The child folder to be removed + * @return True if the removal succeeds + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool RemoveChildFolder(UMovieSceneFolder* TargetFolder, UMovieSceneFolder* FolderToRemove); + + /** + * Get the master tracks contained by this folder + * + * @param Folder The folder to get the master tracks of + * @return The master tracks under the given folder + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static TArray GetChildMasterTracks(UMovieSceneFolder* Folder); + + /** + * Add a master track to this folder + * + * @param Folder The folder to add a child master track to + * @param InMasterTrack The master track to add to the folder + * @return True if the addition is successful + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool AddChildMasterTrack(UMovieSceneFolder* Folder, UMovieSceneTrack* InMasterTrack); + + /** + * Remove a master track from the given folder + * + * @param Folder The folder from which to remove a track + * @param InMasterTrack The track to remove + * @return True if the removal succeeds + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool RemoveChildMasterTrack(UMovieSceneFolder* Folder, UMovieSceneTrack* InMasterTrack); + + /** + * Get the object bindings contained by this folder + * + * @param Folder The folder to get the bindings of + * @return The object bindings under the given folder + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static TArray GetChildObjectBindings(UMovieSceneFolder* Folder); + + /** + * Add a guid for an object binding to this folder + * + * @param Folder The folder to add a child object to + * @param InObjectBinding The binding to add to the folder + * @return True if the addition is successful + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool AddChildObjectBinding(UMovieSceneFolder* Folder, FSequencerBindingProxy InObjectBinding); + + /** + * Remove an object binding from the given folder + * + * @param Folder The folder from which to remove an object binding + * @param InObjectBinding The object binding to remove + * @return True if the operation succeeds + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static bool RemoveChildObjectBinding(UMovieSceneFolder* Folder, const FSequencerBindingProxy InObjectBinding); +}; \ No newline at end of file diff --git a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneSequenceExtensions.h b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneSequenceExtensions.h index 862b63bfc1f0..40ee504c111e 100644 --- a/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneSequenceExtensions.h +++ b/Engine/Plugins/MovieScene/SequencerScripting/Source/SequencerScripting/Public/ExtensionLibraries/MovieSceneSequenceExtensions.h @@ -223,6 +223,25 @@ public: UFUNCTION(BlueprintCallable, Category="Sequence", meta=(ScriptMethod)) static TArray LocateBoundObjects(UMovieSceneSequence* Sequence, const FSequencerBindingProxy& InBinding, UObject* Context); + /** + * Get the root folders in the provided sequence + * + * @param Sequence The sequence to retrieve folders from + * @return The folders contained within the given sequence + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static TArray GetRootFoldersInSequence(UMovieSceneSequence* Sequence); + + /** + * Add a root folder to the given sequence + * + * @param Sequence The sequence to add a folder to + * @param NewFolderName The name to give the added folder + * @return The newly created folder + */ + UFUNCTION(BlueprintCallable, Category="Editor Scripting|Sequencer Tools|Folders", meta=(ScriptMethod)) + static UMovieSceneFolder* AddRootFolderToSequence(UMovieSceneSequence* Sequence, FString NewFolderName); + public: /** diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Classes/ProcessUnitTest.h b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Classes/ProcessUnitTest.h index 7448383c93af..de1ed90881b9 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Classes/ProcessUnitTest.h +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Classes/ProcessUnitTest.h @@ -239,9 +239,11 @@ protected: * * @param InCommandline The commandline that the child process should use * @param bMinimized Starts the process with the window minimized + * @param Type If we are not in Dev Editor mode, the Game/Server processes are separate - specify which here * @return Returns a pointer to the new processes handling struct */ - virtual TWeakPtr StartUE4UnitTestProcess(FString InCommandline, bool bMinimized=true); + virtual TWeakPtr StartUE4UnitTestProcess(FString InCommandline, bool bMinimized=true, + EBuildTargets::Type Type=EBuildTargets::Game); /** * Shuts-down/cleans-up a child process tied to the unit test diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ClientUnitTest.cpp b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ClientUnitTest.cpp index 48613f0c0ebc..b11d19aaa40c 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ClientUnitTest.cpp +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ClientUnitTest.cpp @@ -1165,7 +1165,7 @@ void UClientUnitTest::StartUnitTestServer() ServerParameters += FString::Printf(TEXT(" -BeaconPort=%i -NUTMonitorBeacon"), ServerBeaconPort); } - ServerHandle = StartUE4UnitTestProcess(ServerParameters); + ServerHandle = StartUE4UnitTestProcess(ServerParameters, true, EBuildTargets::Server); if (ServerHandle.IsValid()) { diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/MinimalClient.cpp b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/MinimalClient.cpp index 471394013942..7c46c1bff636 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/MinimalClient.cpp +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/MinimalClient.cpp @@ -205,6 +205,9 @@ FOutBunch* UMinimalClient::CreateChannelBunchByName(const FName& ChName, int32 C check(UnitConn->Channels[ChIndex] == nullptr); } + // Hack to force IsNetReady + UnitConn->QueuedBits = -(MAX_PACKET_SIZE * 8); + if (ControlChan != nullptr && ControlChan->IsNetReady(false)) { int32 BunchSequence = ++UnitConn->OutReliable[ChIndex]; @@ -215,7 +218,9 @@ FOutBunch* UMinimalClient::CreateChannelBunchByName(const FName& ChName, int32 C ReturnVal->Time = 0.0; ReturnVal->ReceivedAck = false; ReturnVal->PacketId = 0; + PRAGMA_DISABLE_DEPRECATION_WARNINGS ReturnVal->bDormant = false; + PRAGMA_ENABLE_DEPRECATION_WARNINGS ReturnVal->Channel = nullptr; ReturnVal->ChIndex = ChIndex; ReturnVal->ChName = ChName; @@ -294,7 +299,10 @@ bool UMinimalClient::SendRawBunch(FOutBunch& Bunch, bool bAllowPartial/*=false*/ NewBunch->bReliable = Bunch.bReliable; NewBunch->bOpen = Bunch.bOpen; NewBunch->bClose = Bunch.bClose; + PRAGMA_DISABLE_DEPRECATION_WARNINGS NewBunch->bDormant = Bunch.bDormant; + PRAGMA_ENABLE_DEPRECATION_WARNINGS + NewBunch->CloseReason = Bunch.CloseReason; NewBunch->bIsReplicationPaused = Bunch.bIsReplicationPaused; NewBunch->ChIndex = Bunch.ChIndex; NewBunch->ChName = Bunch.ChName; diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/NetcodeUnitTest.cpp b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/NetcodeUnitTest.cpp index 1173b221ea69..9fad6350580f 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/NetcodeUnitTest.cpp +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/NetcodeUnitTest.cpp @@ -102,6 +102,14 @@ public: DumpRPCsHandle = FProcessEventHook::Get().AddGlobalRPCHook(FOnProcessNetEvent::CreateStatic(&FNetcodeUnitTest::DumpRPC)); } + // Add earlier log trace opportunity (execcmds is sometimes too late) + FString TraceStr; + + if (FParse::Value(FCommandLine::Get(), TEXT("LogTrace="), TraceStr) && TraceStr.Len() > 0) + { + GLogTraceManager->AddLogTrace(TraceStr); + } + // Hack-override the log category name #if !NO_LOGGING class FLogOverride : public FLogCategoryBase diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ProcessUnitTest.cpp b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ProcessUnitTest.cpp index f0fc3b6ff45f..8154f9d7ea1c 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ProcessUnitTest.cpp +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/ProcessUnitTest.cpp @@ -77,7 +77,7 @@ TWeakPtr UProcessUnitTest::StartUnitTestProcess(FString Path, verify(FPlatformProcess::CreatePipe(ReturnVal->ReadPipe, ReturnVal->WritePipe)); - UNIT_LOG(ELogType::StatusImportant, TEXT("Starting process with parameters: %s"), *InCommandline); + UNIT_LOG(ELogType::StatusImportant, TEXT("Starting process '%s' with parameters: %s"), *Path, *InCommandline); ReturnVal->ProcessHandle = FPlatformProcess::CreateProc(*Path, *InCommandline, true, bMinimized, false, &ReturnVal->ProcessID, 0, NULL, ReturnVal->WritePipe); @@ -91,16 +91,32 @@ TWeakPtr UProcessUnitTest::StartUnitTestProcess(FString Path, } else { - UNIT_LOG(ELogType::StatusFailure, TEXT("Failed to start process")); + UNIT_LOG(ELogType::StatusFailure, TEXT("Failed to start process: %s (%s)"), *Path, *InCommandline); } return ReturnVal; } -TWeakPtr UProcessUnitTest::StartUE4UnitTestProcess(FString InCommandline, bool bMinimized/*=true*/) +TWeakPtr UProcessUnitTest::StartUE4UnitTestProcess(FString InCommandline, bool bMinimized/*=true*/, + EBuildTargets::Type Type/*=EBuildTargets::Game*/) { - TWeakPtr ReturnVal = NULL; - FString GamePath = FPlatformProcess::GenerateApplicationPath(FApp::GetName(), FApp::GetBuildConfiguration()); + TWeakPtr ReturnVal = nullptr; + FString TargetExecutable = FApp::GetName(); + +#if UE_BUILD_DEVELOPMENT && !WITH_EDITOR + // Development modes other than Dev Editor, must target the separate Server process + if (Type == EBuildTargets::Server) + { + TargetExecutable = TargetExecutable.Replace(TEXT("Game"), TEXT("Server")); + + UNIT_LOG(, TEXT("Targeting server process '%s'. If this fails, make sure you compiled Development Server and cooked it in UnrealFrontend."), + *TargetExecutable); + } + + FString GamePath = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Binaries"), FPlatformProcess::GetBinariesSubdirectory(), TargetExecutable); +#else + FString GamePath = FPlatformProcess::GenerateApplicationPath(TargetExecutable, FApp::GetBuildConfiguration()); +#endif ReturnVal = StartUnitTestProcess(GamePath, InCommandline, bMinimized); diff --git a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/UnitTestCommandlet.cpp b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/UnitTestCommandlet.cpp index c5b92673ed00..bca0ecf19f79 100644 --- a/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/UnitTestCommandlet.cpp +++ b/Engine/Plugins/NetcodeUnitTest/NetcodeUnitTest/Source/NetcodeUnitTest/Private/UnitTestCommandlet.cpp @@ -63,9 +63,11 @@ UUnitTestCommandlet::UUnitTestCommandlet(const FObjectInitializer& ObjectInitial int32 UUnitTestCommandlet::Main(const FString& Params) { - // @todo #JohnBLowPri: Fix StandaloneRenderer support for static builds + // @todo #JohnBLowPri: Fix StandaloneRenderer support for static builds (this is very hard to do, + // getting the game renderer going with commandlets, is very tricky) #if IS_MONOLITHIC - UE_LOG(LogUnitTest, Log, TEXT("NetcodeUnitTest commandlet not currently supported in static/monolithic builds")); + UE_LOG(LogUnitTest, Log, TEXT("NetcodeUnitTest commandlet not currently supported in static/monolithic builds. ") + TEXT("You must disable the externs in SlateOpenGLExtensions.cpp to hack-unblock monolithic builds.")); #else GIsRequestingExit = false; diff --git a/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Private/OnlineStoreIOS.cpp b/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Private/OnlineStoreIOS.cpp index 421df124d63d..3739b1c903b3 100644 --- a/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Private/OnlineStoreIOS.cpp +++ b/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Private/OnlineStoreIOS.cpp @@ -59,6 +59,31 @@ void FOnlineStoreIOS::QueryOffersByFilter(const FUniqueNetId& UserId, const FOnl Delegate.ExecuteIfBound(false, TArray(), TEXT("No CatalogService")); } +bool FOnlineStoreIOS::OffersNotAllowedInLocale(const FString& InLocale) +{ + UE_LOG_ONLINE_STOREV2(Log, TEXT("Locale: %s"), *InLocale); + // get the data from the config file + TArray BannedLocales; + GConfig->GetArray(TEXT("OnlineSubsystemIOS.Store"), TEXT("BannedLocales"), BannedLocales, GEngineIni); + if (BannedLocales.Num() == 0) + { + // no banned locales just let the offer proceed + return false; + } + + TArray LocaleData; + InLocale.ParseIntoArray(LocaleData, TEXT("-")); + FString Locale = LocaleData.Num() > 1 ? LocaleData[1] : LocaleData[0]; + for (FString BannedLocale : BannedLocales) + { + if (BannedLocale == Locale) + { + return true; + } + } + return false; +} + void FOnlineStoreIOS::QueryOffersById(const FUniqueNetId& UserId, const TArray& OfferIds, const FOnQueryOnlineStoreOffersComplete& Delegate) { UE_LOG_ONLINE_STOREV2(Verbose, TEXT("FOnlineStoreIOS::QueryOffersById")); @@ -71,6 +96,11 @@ void FOnlineStoreIOS::QueryOffersById(const FUniqueNetId& UserId, const TArray OfferedIds; + Delegate.ExecuteIfBound(true, OfferedIds, TEXT("")); + } else { // autoreleased NSSet to hold IDs diff --git a/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Public/OnlineStoreIOS.h b/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Public/OnlineStoreIOS.h index 25afcd814c03..b48105b3c7f8 100644 --- a/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Public/OnlineStoreIOS.h +++ b/Engine/Plugins/Online/IOS/OnlineSubsystemIOS/Source/Public/OnlineStoreIOS.h @@ -117,6 +117,8 @@ private: void AddOffer(const FOnlineStoreOfferIOS& NewOffer); + bool OffersNotAllowedInLocale(const FString& Locale); + private: /** delegate fired when a product request completes */ diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Private/OnlineHotfixManager.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Private/OnlineHotfixManager.cpp index b2f1faacce72..fd91c4edafa9 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Private/OnlineHotfixManager.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Private/OnlineHotfixManager.cpp @@ -3,7 +3,6 @@ #include "OnlineHotfixManager.h" #include "OnlineSubsystemUtils.h" #include "GenericPlatform/GenericPlatformFile.h" -#include "Internationalization/Culture.h" #include "UObject/UObjectIterator.h" #include "UObject/Package.h" #include "Http.h" @@ -97,9 +96,9 @@ namespace * If the file has version information it is compared with compatibility * If the file has NO version information it is assumed compatible * - * @param InFilename name of the file to check + * @param InFilename name of the file to check * @param OutFilename name of file with version information stripped - * + * * @return true if file is compatible, false otherwise */ bool IsCompatibleHotfixFile(const FString& InFilename, FString& OutFilename) @@ -196,9 +195,6 @@ void UOnlineHotfixManager::Init() TotalBytes = 0; NumBytes = 0; ChangedOrRemovedPakCount = 0; - // Build the name of the loc file that we'll care about - // It can change at runtime so build it just before fetching the data - GameLocName = DebugPrefix + FInternationalization::Get().GetCurrentCulture()->GetTwoLetterISOLanguageName() + TEXT("_Game.locres"); OnlineTitleFile = Online::GetTitleFileInterface(OSSName.Len() ? FName(*OSSName, FNAME_Find) : NAME_None); if (OnlineTitleFile.IsValid()) { @@ -272,7 +268,7 @@ struct FHotfixFileSortPredicate { // Non-ini files are applied last int32 Priority = 50; - + if (InHotfixName.EndsWith(TEXT("INI"))) { FString HotfixName, NetworkVersion; @@ -433,7 +429,7 @@ void UOnlineHotfixManager::OnEnumerateFilesForAvailabilityComplete(bool bWasSucc { OnlineTitleFile->ClearOnEnumerateFilesCompleteDelegate_Handle(OnEnumerateFilesForAvailabilityCompleteDelegateHandle); } - + EHotfixResult Result = EHotfixResult::Failed; if (bWasSuccessful) { @@ -506,9 +502,9 @@ void UOnlineHotfixManager::BuildHotfixFileListDeltas() { bool bFoundMatch = HotfixFileList.ContainsByPredicate( [&LastHeader](const FCloudFileHeader& CurrentHeader) - { - return LastHeader.FileName == CurrentHeader.FileName; - }); + { + return LastHeader.FileName == CurrentHeader.FileName; + }); if (!bFoundMatch) { // We've been removed so add to the removed list @@ -649,7 +645,7 @@ void UOnlineHotfixManager::ApplyHotfix() void UOnlineHotfixManager::TriggerHotfixComplete(EHotfixResult HotfixResult) { - if( HotfixResult != EHotfixResult::Failed ) + if (HotfixResult != EHotfixResult::Failed) { PatchAssetsFromIniFiles(); } @@ -701,7 +697,7 @@ bool UOnlineHotfixManager::WantsHotfixProcessing(const FCloudFileHeader& FileHea { return FileHeader.FileName.Find(PlatformPrefix) != -1; } - return FileHeader.FileName == GameLocName; + return false; } bool UOnlineHotfixManager::ApplyHotfixProcessing(const FCloudFileHeader& FileHeader) @@ -733,12 +729,6 @@ bool UOnlineHotfixManager::ApplyHotfixProcessing(const FCloudFileHeader& FileHea UE_LOG(LogHotfixManager, Warning, TEXT("Failed to get contents of %s"), *FileHeader.FileName); } } - else if (Extension == TEXT("LOCRES")) - { - HotfixLocFile(FileHeader); - // Currently no failure case for this - bSuccess = true; - } else if (Extension == TEXT("PAK")) { bSuccess = HotfixPakFile(FileHeader); @@ -1021,14 +1011,6 @@ bool UOnlineHotfixManager::HotfixIniFile(const FString& FileName, const FString& return true; } -void UOnlineHotfixManager::HotfixLocFile(const FCloudFileHeader& FileHeader) -{ - const double StartTime = FPlatformTime::Seconds(); - FString LocFilePath = FString::Printf(TEXT("%s/%s"), *GetCachedDirectory(), *FileHeader.DLName); - FTextLocalizationManager::Get().UpdateFromLocalizationResource(LocFilePath); - UE_LOG(LogHotfixManager, Log, TEXT("Updating loc from %s took %f seconds"), *FileHeader.FileName, FPlatformTime::Seconds() - StartTime); -} - bool UOnlineHotfixManager::HotfixPakFile(const FCloudFileHeader& FileHeader) { if (!FCoreDelegates::OnMountPak.IsBound()) @@ -1213,7 +1195,7 @@ void UOnlineHotfixManager::OnReadFileProgress(const FString& FileName, uint64 By UOnlineHotfixManager::FConfigFileBackup& UOnlineHotfixManager::BackupIniFile(const FString& IniName, const FConfigFile* ConfigFile) { FString BackupIniName = GetConfigFileNamePath(GetStrippedConfigFileName(IniName)); - if (FConfigFileBackup* Backup = IniBackups.FindByPredicate([&BackupIniName](const FConfigFileBackup& Entry){ return Entry.IniName == BackupIniName; })) + if (FConfigFileBackup* Backup = IniBackups.FindByPredicate([&BackupIniName](const FConfigFileBackup& Entry) { return Entry.IniName == BackupIniName; })) { // Only store one copy of each ini file, consisting of the original state return *Backup; @@ -1318,16 +1300,16 @@ void UOnlineHotfixManager::RestoreBackupIniFiles() void UOnlineHotfixManager::PatchAssetsFromIniFiles() { - UE_LOG( LogHotfixManager, Display, TEXT( "Checking for assets to be patched using data from 'AssetHotfix' section in the Game .ini file" ) ); + UE_LOG(LogHotfixManager, Display, TEXT("Checking for assets to be patched using data from 'AssetHotfix' section in the Game .ini file")); int32 TotalPatchableAssets = 0; AssetsHotfixedFromIniFiles.Reset(); // Everything should be under the 'AssetHotfix' section in Game.ini - FConfigSection* AssetHotfixConfigSection = GConfig->GetSectionPrivate( TEXT( "AssetHotfix" ), false, true, GGameIni ); - if( AssetHotfixConfigSection != nullptr ) + FConfigSection* AssetHotfixConfigSection = GConfig->GetSectionPrivate(TEXT("AssetHotfix"), false, true, GGameIni); + if (AssetHotfixConfigSection != nullptr) { - for( FConfigSection::TIterator It( *AssetHotfixConfigSection ); It; ++It ) + for (FConfigSection::TIterator It(*AssetHotfixConfigSection); It; ++It) { ++TotalPatchableAssets; @@ -1341,9 +1323,9 @@ void UOnlineHotfixManager::PatchAssetsFromIniFiles() // Make sure the entry has a valid class name that we supprt UClass* AssetClass = nullptr; - for( UClass* PatchableAssetClass : PatchableAssetClasses ) + for (UClass* PatchableAssetClass : PatchableAssetClasses) { - if( PatchableAssetClass && (It.Key() == PatchableAssetClass->GetFName()) ) + if (PatchableAssetClass && (It.Key() == PatchableAssetClass->GetFName())) { AssetClass = PatchableAssetClass; break; @@ -1385,7 +1367,7 @@ void UOnlineHotfixManager::PatchAssetsFromIniFiles() // The hotfix line should be // +DataTable=;TableUpdate;"" // +CurveTable=;TableUpdate;"" - + // We have to read json data as quoted string because tokenizing it creates extra unwanted characters. FString JsonData; if (FParse::QuotedString(*Tokens[2], JsonData)) @@ -1432,17 +1414,17 @@ void UOnlineHotfixManager::PatchAssetsFromIniFiles() } } - if( TotalPatchableAssets == 0 ) + if (TotalPatchableAssets == 0) { - UE_LOG( LogHotfixManager, Display, TEXT( "No assets were found in the 'AssetHotfix' section in the Game .ini file. No patching needed." ) ); + UE_LOG(LogHotfixManager, Display, TEXT("No assets were found in the 'AssetHotfix' section in the Game .ini file. No patching needed.")); } - else if( TotalPatchableAssets == AssetsHotfixedFromIniFiles.Num() ) + else if (TotalPatchableAssets == AssetsHotfixedFromIniFiles.Num()) { - UE_LOG( LogHotfixManager, Display, TEXT( "Successfully patched all %i assets from the 'AssetHotfix' section in the Game .ini file. These assets will be forced to remain loaded." ), AssetsHotfixedFromIniFiles.Num() ); + UE_LOG(LogHotfixManager, Display, TEXT("Successfully patched all %i assets from the 'AssetHotfix' section in the Game .ini file. These assets will be forced to remain loaded."), AssetsHotfixedFromIniFiles.Num()); } else { - UE_LOG( LogHotfixManager, Error, TEXT( "Only %i of %i assets were successfully patched from 'AssetHotfix' section in the Game .ini file. The patched assets will be forced to remain loaded. Any assets that failed to patch may be left in an invalid state!" ), AssetsHotfixedFromIniFiles.Num(), TotalPatchableAssets ); + UE_LOG(LogHotfixManager, Error, TEXT("Only %i of %i assets were successfully patched from 'AssetHotfix' section in the Game .ini file. The patched assets will be forced to remain loaded. Any assets that failed to patch may be left in an invalid state!"), AssetsHotfixedFromIniFiles.Num(), TotalPatchableAssets); } } @@ -1485,17 +1467,17 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP UStrProperty* StrProp = Cast(DataTableRowProperty); UNameProperty* NameProp = Cast(DataTableRowProperty); USoftObjectProperty* SoftObjProp = Cast(DataTableRowProperty); - + // Get the row data by name. static const FString Context = FString(TEXT("UOnlineHotfixManager::PatchAssetsFromIniFiles")); FTableRowBase* DataTableRow = DataTable->FindRow(FName(*RowName), Context); if (DataTableRow) { - // Numeric property - if (NumProp) + uint8* RowData = DataTableRowProperty->ContainerPtrToValuePtr(DataTableRow, 0); + if (RowData) { - void* RowData = NumProp->ContainerPtrToValuePtr(DataTableRow, 0); - if (RowData) + // Numeric property + if (NumProp) { if (NewValue.IsNumeric()) { @@ -1525,18 +1507,8 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP ProblemStrings.Add(Problem); } } - // Row data wasn't found. - else - { - const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName)); - ProblemStrings.Add(Problem); - } - } - // String property - else if (StrProp) - { - void* RowData = StrProp->ContainerPtrToValuePtr(DataTableRow, 0); - if (RowData) + // String property + else if (StrProp) { const FString OldPropertyValue = StrProp->GetPropertyValue(RowData); const FString NewPropertyValue = NewValue; @@ -1544,18 +1516,8 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP bWasDataTableChanged = true; UE_LOG(LogHotfixManager, Verbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue, *NewPropertyValue); } - // Row data wasn't found. - else - { - const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName)); - ProblemStrings.Add(Problem); - } - } - // FName property - else if (NameProp) - { - void* RowData = NameProp->ContainerPtrToValuePtr(DataTableRow, 0); - if (RowData) + // FName property + else if (NameProp) { const FName OldPropertyValue = NameProp->GetPropertyValue(RowData); const FName NewPropertyValue = FName(*NewValue); @@ -1563,18 +1525,8 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP bWasDataTableChanged = true; UE_LOG(LogHotfixManager, Verbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue.ToString(), *NewPropertyValue.ToString()); } - // Row data wasn't found. - else - { - const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName)); - ProblemStrings.Add(Problem); - } - } - // Soft Object property - else if (SoftObjProp) - { - void* RowData = SoftObjProp->ContainerPtrToValuePtr(DataTableRow, 0); - if (RowData) + // Soft Object property + else if (SoftObjProp) { const UObject* OldPropertyValue = SoftObjProp->GetObjectPropertyValue(RowData); UObject* NewPropertyValue = LoadObject(nullptr, *NewValue); @@ -1582,17 +1534,24 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP bWasDataTableChanged = true; UE_LOG(LogHotfixManager, Verbose, TEXT("Data table %s row %s updated column %s from %s to %s."), *AssetPath, *RowName, *ColumnName, *OldPropertyValue->GetFullName(), *NewPropertyValue->GetFullName()); } - // Row data wasn't found. + // Not an expected property. else { - const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName)); - ProblemStrings.Add(Problem); + // we'll make one last attempt here + FString Error = DataTableUtils::AssignStringToProperty(NewValue, DataTableRowProperty, (uint8*)DataTableRow); + + if (Error.Len() > 0) + { + const FString Problem(FString::Printf(TEXT("The data table row property named %s is not a UNumericProperty, UStrProperty, UNameProperty, or USoftObjectProperty and it should be."), *ColumnName)); + ProblemStrings.Add(Problem); + ProblemStrings.Add(FString::Printf(TEXT("%s"), *Error)); + } } } - // Not an expected property. + // Row data wasn't found. else { - const FString Problem(FString::Printf(TEXT("The data table row property named %s is not a UNumericProperty, UStrProperty, UNameProperty, or USoftObjectProperty and it should be."), *ColumnName)); + const FString Problem(FString::Printf(TEXT("The data table row data for row %s was not found."), *RowName)); ProblemStrings.Add(Problem); } } @@ -1623,8 +1582,8 @@ void UOnlineHotfixManager::HotfixRowUpdate(UObject* Asset, const FString& AssetP { // Get the row data by name. static const FString Context = FString(TEXT("UOnlineHotfixManager::PatchAssetsFromIniFiles")); - FRichCurve* CurveTableRow = CurveTable->FindCurve(FName(*RowName), Context); - + FRealCurve* CurveTableRow = CurveTable->FindCurve(FName(*RowName), Context); + if (CurveTableRow) { // Edit the row with the new value. @@ -1828,4 +1787,4 @@ struct FHotfixManagerExec : return false; } }; -static FHotfixManagerExec HotfixManagerExec; +static FHotfixManagerExec HotfixManagerExec; \ No newline at end of file diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Public/OnlineHotfixManager.h b/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Public/OnlineHotfixManager.h index 64aca2d207d1..9f3726f81f6c 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Public/OnlineHotfixManager.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Hotfix/Public/OnlineHotfixManager.h @@ -141,8 +141,6 @@ protected: TArray MountedPakFiles; /** Backup copies of INI files that change during hotfixing so they can be undone afterward */ TArray IniBackups; - /** Holds the game localization resource name that we should search for */ - FString GameLocName; /** Used to match any PAK files for this platform */ FString PlatformPrefix; /** Used to match any server-only hotfixes */ @@ -249,14 +247,6 @@ protected: * @return whether the merging was successful or not */ virtual bool HotfixIniFile(const FString& FileName, const FString& IniData); - /** - * Override this to change the default loc file handling: - * - * @param FileName - the name of the loc file being merged into the loc manager - * - * @return whether the mounting of the PAK file was successful or not - */ - virtual void HotfixLocFile(const FCloudFileHeader& FileHeader); /** * Override this to change the default PAK file handling: * - mount PAK file immediately diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Chat/SocialChatChannel.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Chat/SocialChatChannel.cpp index 2659442dffdb..509def750b70 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Chat/SocialChatChannel.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Chat/SocialChatChannel.cpp @@ -17,7 +17,11 @@ void USocialChatChannel::NotifyUserJoinedChannel(USocialUser& User) { static FText UserJoinedMessage = LOCTEXT("SocialChatRoom_MemberJoined", "{0} has joined."); - //AddSystemMessage(FText::Format(UserJoinedMessage, FText::FromString(User.GetDisplayName())).ToString()); + if (ChannelType == ESocialChannelType::Party || + ChannelType == ESocialChannelType::Team) + { + AddSystemMessage(FText::Format(UserJoinedMessage, FText::FromString(User.GetDisplayName()))); + } OnUserJoinedChannel().Broadcast(User); } @@ -26,7 +30,11 @@ void USocialChatChannel::NotifyUserLeftChannel(USocialUser& User) { static FText UserLeftMessage = LOCTEXT("SocialChatRoom_MemberExit", "{0} has left."); - //AddSystemMessage(FText::Format(UserLeftMessage, FText::FromString(User.GetDisplayName())).ToString()); + if (ChannelType == ESocialChannelType::Party || + ChannelType == ESocialChannelType::Team) + { + AddSystemMessage(FText::Format(UserLeftMessage, FText::FromString(User.GetDisplayName()))); + } OnUserLeftChannel().Broadcast(User); } @@ -61,9 +69,9 @@ void USocialChatChannel::SanitizeMessage(FString& RawMessage) const RawMessage.ReplaceInline(TEXT(">"), TEXT(">")); } -void USocialChatChannel::AddSystemMessage(const FString& MessageBody) +void USocialChatChannel::AddSystemMessage(const FText& MessageBody) { - AddMessageInternal(FSocialSystemChatMessage::Create(TEXT("System"), MessageBody, ChannelType, EChatSystemMessagePurpose::Info)); + AddMessageInternal(FSocialSystemChatMessage::Create(TEXT("System"), MessageBody.ToString(), ChannelType, EChatSystemMessagePurpose::Info)); } void USocialChatChannel::AddMessageInternal(FSocialChatMessageRef NewMessage) diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/CoreInteractions.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/CoreInteractions.cpp index 40c1e51b5bf1..29663319652c 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/CoreInteractions.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/CoreInteractions.cpp @@ -17,8 +17,12 @@ // AddFriend ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_AddFriend::GetDisplayName() +FText FSocialInteraction_AddFriend::GetDisplayName(const USocialUser& User) { + if (User.IsFriend(ESocialSubsystem::Platform)) + { + return LOCTEXT("AddEpicFriend", "Add Epic Friend"); + } return LOCTEXT("AddFriend", "Add Friend"); } @@ -27,29 +31,51 @@ FString FSocialInteraction_AddFriend::GetSlashCommandToken() return TEXT("friend"); } -void FSocialInteraction_AddFriend::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_AddFriend::CanExecute(const USocialUser& User) { - if (!User.IsFriend(ESocialSubsystem::Primary)) - { - if (User.GetFriendInviteStatus(ESocialSubsystem::Primary) != EInviteStatus::PendingInbound && !User.IsBlocked(ESocialSubsystem::Primary)) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - } - //@todo DanH: Need to sort out display name differentiation between the same interaction on two subsystems. ViewProfile covers this nicely for now #future - /*if (!User.IsFriend(ESocialSubsystem::Platform) && User.HasSubsystemInfo(ESocialSubsystem::Platform)) - { - const FName PlatformSubsystemName = USocialManager::GetSocialOssName(ESocialSubsystem::Platform); - if (PlatformSubsystemName == LIVE_SUBSYSTEM || PlatformSubsystemName == PS4_SUBSYSTEM) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } - }*/ + return User.CanSendFriendInvite(ESocialSubsystem::Primary); } -void FSocialInteraction_AddFriend::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_AddFriend::ExecuteInteraction(USocialUser& User) { - User.SendFriendInvite(SocialSubsystem); + User.SendFriendInvite(ESocialSubsystem::Primary); +} + +////////////////////////////////////////////////////////////////////////// +// AddPlatformFriend +////////////////////////////////////////////////////////////////////////// + +FText FSocialInteraction_AddPlatformFriend::GetDisplayName(const USocialUser& User) +{ + const FName PlatformOssName = USocialManager::GetSocialOssName(ESocialSubsystem::Platform); + if (PlatformOssName == LIVE_SUBSYSTEM) + { + return LOCTEXT("AddPlatformFriend_Live", "Add Xbox Live Friend"); + } + else if (PlatformOssName == PS4_SUBSYSTEM) + { + return LOCTEXT("AddPlatformFriend_PSN", "Add Playstation Network Friend"); + } + else if (PlatformOssName == TENCENT_SUBSYSTEM) + { + return LOCTEXT("AddPlatformFriend_Tencent", "Add WeGame Friend"); + } + return LOCTEXT("AddPlatformFriend_Unknown", "Add Platform Friend"); +} + +FString FSocialInteraction_AddPlatformFriend::GetSlashCommandToken() +{ + return TEXT(""); +} + +bool FSocialInteraction_AddPlatformFriend::CanExecute(const USocialUser& User) +{ + return User.CanSendFriendInvite(ESocialSubsystem::Platform); +} + +void FSocialInteraction_AddPlatformFriend::ExecuteInteraction(USocialUser& User) +{ + User.SendFriendInvite(ESocialSubsystem::Platform); } ////////////////////////////////////////////////////////////////////////// @@ -57,7 +83,7 @@ void FSocialInteraction_AddFriend::ExecuteAction(ESocialSubsystem SocialSubsyste ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_RemoveFriend::GetDisplayName() +FText FSocialInteraction_RemoveFriend::GetDisplayName(const USocialUser& User) { return LOCTEXT("RemoveFriend", "Remove Friend"); } @@ -67,25 +93,22 @@ FString FSocialInteraction_RemoveFriend::GetSlashCommandToken() return TEXT("unfriend"); } -void FSocialInteraction_RemoveFriend::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_RemoveFriend::CanExecute(const USocialUser& User) { - if (User.IsFriend(ESocialSubsystem::Primary)) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return User.IsFriend(ESocialSubsystem::Primary); } -void FSocialInteraction_RemoveFriend::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_RemoveFriend::ExecuteInteraction(USocialUser& User) { //@todo DanH SocialInteractions: Same issue as parties - there can be N different named friends lists. Whatever we do for different party types, do here too. #future - User.EndFriendship(SocialSubsystem); + User.EndFriendship(ESocialSubsystem::Primary); } ////////////////////////////////////////////////////////////////////////// // AcceptFriendInvite ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_AcceptFriendInvite::GetDisplayName() +FText FSocialInteraction_AcceptFriendInvite::GetDisplayName(const USocialUser& User) { return LOCTEXT("AcceptFriendInvite", "Accept"); } @@ -95,28 +118,21 @@ FString FSocialInteraction_AcceptFriendInvite::GetSlashCommandToken() return TEXT("accept"); } -void FSocialInteraction_AcceptFriendInvite::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_AcceptFriendInvite::CanExecute(const USocialUser& User) { - if (User.GetFriendInviteStatus(ESocialSubsystem::Primary) == EInviteStatus::PendingInbound) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - if (User.GetFriendInviteStatus(ESocialSubsystem::Platform) == EInviteStatus::PendingInbound) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } + return User.GetFriendInviteStatus(ESocialSubsystem::Primary) == EInviteStatus::PendingInbound; } -void FSocialInteraction_AcceptFriendInvite::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_AcceptFriendInvite::ExecuteInteraction(USocialUser& User) { - User.AcceptFriendInvite(SocialSubsystem); + User.AcceptFriendInvite(ESocialSubsystem::Primary); } ////////////////////////////////////////////////////////////////////////// // RejectFriendInvite ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_RejectFriendInvite::GetDisplayName() +FText FSocialInteraction_RejectFriendInvite::GetDisplayName(const USocialUser& User) { return LOCTEXT("RejectFriendInvite", "Reject"); } @@ -126,28 +142,21 @@ FString FSocialInteraction_RejectFriendInvite::GetSlashCommandToken() return TEXT("reject"); } -void FSocialInteraction_RejectFriendInvite::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_RejectFriendInvite::CanExecute(const USocialUser& User) { - if (User.GetFriendInviteStatus(ESocialSubsystem::Primary) == EInviteStatus::PendingInbound) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - if (User.GetFriendInviteStatus(ESocialSubsystem::Platform) == EInviteStatus::PendingInbound) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } + return User.GetFriendInviteStatus(ESocialSubsystem::Primary) == EInviteStatus::PendingInbound; } -void FSocialInteraction_RejectFriendInvite::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_RejectFriendInvite::ExecuteInteraction(USocialUser& User) { - User.RejectFriendInvite(SocialSubsystem); + User.RejectFriendInvite(ESocialSubsystem::Primary); } ////////////////////////////////////////////////////////////////////////// // Block ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_Block::GetDisplayName() +FText FSocialInteraction_Block::GetDisplayName(const USocialUser& User) { return LOCTEXT("BlockUser", "Block"); } @@ -157,37 +166,21 @@ FString FSocialInteraction_Block::GetSlashCommandToken() return TEXT("block"); } -void FSocialInteraction_Block::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_Block::CanExecute(const USocialUser& User) { - if (User.HasSubsystemInfo(ESocialSubsystem::Primary)) - { - // If there is a primary subsystem, only bother with blocking there - if (!User.IsBlocked(ESocialSubsystem::Primary)) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - } - else if (User.HasSubsystemInfo(ESocialSubsystem::Platform) && !User.IsBlocked(ESocialSubsystem::Platform)) - { - // If the platform subsystem is the only one available, allow that instead (so long as it's xbox or ps4) - const FName PlatformSubsystemName = USocialManager::GetSocialOssName(ESocialSubsystem::Platform); - if (PlatformSubsystemName == LIVE_SUBSYSTEM || PlatformSubsystemName == PS4_SUBSYSTEM) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } - } + return User.HasSubsystemInfo(ESocialSubsystem::Primary) && !User.IsBlocked(ESocialSubsystem::Primary); } -void FSocialInteraction_Block::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_Block::ExecuteInteraction(USocialUser& User) { - User.BlockUser(SocialSubsystem); + User.BlockUser(ESocialSubsystem::Primary); } ////////////////////////////////////////////////////////////////////////// // Unblock ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_Unblock::GetDisplayName() +FText FSocialInteraction_Unblock::GetDisplayName(const USocialUser& User) { return LOCTEXT("UnblockUser", "Unblock"); } @@ -197,30 +190,14 @@ FString FSocialInteraction_Unblock::GetSlashCommandToken() return TEXT("unblock"); } -void FSocialInteraction_Unblock::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_Unblock::CanExecute(const USocialUser& User) { - if (User.HasSubsystemInfo(ESocialSubsystem::Primary)) - { - // If there is a primary subsystem, only bother with blocking there - if (User.IsBlocked(ESocialSubsystem::Primary)) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - } - else if (User.HasSubsystemInfo(ESocialSubsystem::Platform) && User.IsBlocked(ESocialSubsystem::Platform)) - { - // If the platform subsystem is the only one available, allow that instead (so long as it's xbox or ps4) - const FName PlatformSubsystemName = USocialManager::GetSocialOssName(ESocialSubsystem::Platform); - if (PlatformSubsystemName == LIVE_SUBSYSTEM || PlatformSubsystemName == PS4_SUBSYSTEM) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } - } + return User.HasSubsystemInfo(ESocialSubsystem::Primary) && User.IsBlocked(ESocialSubsystem::Primary); } -void FSocialInteraction_Unblock::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_Unblock::ExecuteInteraction(USocialUser& User) { - User.UnblockUser(SocialSubsystem); + User.UnblockUser(ESocialSubsystem::Primary); } ////////////////////////////////////////////////////////////////////////// @@ -228,7 +205,7 @@ void FSocialInteraction_Unblock::ExecuteAction(ESocialSubsystem SocialSubsystem, ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_PrivateMessage::GetDisplayName() +FText FSocialInteraction_PrivateMessage::GetDisplayName(const USocialUser& User) { return LOCTEXT("SendPrivateMessage", "Whisper"); } @@ -238,24 +215,15 @@ FString FSocialInteraction_PrivateMessage::GetSlashCommandToken() return TEXT("whisper"); } -void FSocialInteraction_PrivateMessage::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_PrivateMessage::CanExecute(const USocialUser& User) { - if (User.GetOwningToolkit().GetChatManager().IsChatRestricted()) - { - return; - } - - // Whispering only takes place on the primary subsystem, but is enabled for friends on both primary and platform subsystems - if (User.GetOnlineStatus() != EOnlinePresenceState::Offline) - { - if (User.IsFriend(ESocialSubsystem::Primary) || User.IsFriend(ESocialSubsystem::Platform)) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } - } + // Whispering only takes place on the primary subsystem, but is enabled for friends on any subsystem + return !User.GetOwningToolkit().GetChatManager().IsChatRestricted() && + User.GetOnlineStatus() != EOnlinePresenceState::Offline && + User.IsFriend(); } -void FSocialInteraction_PrivateMessage::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_PrivateMessage::ExecuteInteraction(USocialUser& User) { USocialChatManager& ChatManager = User.GetOwningToolkit().GetChatManager(); ChatManager.CreateChatChannel(User); @@ -266,7 +234,7 @@ void FSocialInteraction_PrivateMessage::ExecuteAction(ESocialSubsystem SocialSub // ShowPlatformProfile ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_ShowPlatformProfile::GetDisplayName() +FText FSocialInteraction_ShowPlatformProfile::GetDisplayName(const USocialUser& User) { return LOCTEXT("ShowPlatformProfile", "View Profile"); } @@ -276,15 +244,12 @@ FString FSocialInteraction_ShowPlatformProfile::GetSlashCommandToken() return TEXT("profile"); } -void FSocialInteraction_ShowPlatformProfile::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_ShowPlatformProfile::CanExecute(const USocialUser& User) { - if (USocialManager::GetLocalUserPlatform().IsConsole() && User.GetUserId(ESocialSubsystem::Platform).IsValid()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Platform); - } + return USocialManager::GetLocalUserPlatform().IsConsole() && User.GetUserId(ESocialSubsystem::Platform).IsValid(); } -void FSocialInteraction_ShowPlatformProfile::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_ShowPlatformProfile::ExecuteInteraction(USocialUser& User) { User.ShowPlatformProfile(); } diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/PartyInteractions.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/PartyInteractions.cpp index a7e7ffd2387b..aab0bcf454ee 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/PartyInteractions.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/PartyInteractions.cpp @@ -14,7 +14,7 @@ // InviteToParty ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_InviteToParty::GetDisplayName() +FText FSocialInteraction_InviteToParty::GetDisplayName(const USocialUser& User) { return LOCTEXT("InviteToParty", "Invite to Party"); } @@ -24,15 +24,12 @@ FString FSocialInteraction_InviteToParty::GetSlashCommandToken() return TEXT("invite"); } -void FSocialInteraction_InviteToParty::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_InviteToParty::CanExecute(const USocialUser& User) { - if (User.CanInviteToParty(IOnlinePartySystem::GetPrimaryPartyTypeId()) && !User.IsBlocked()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return User.CanInviteToParty(IOnlinePartySystem::GetPrimaryPartyTypeId()); } -void FSocialInteraction_InviteToParty::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_InviteToParty::ExecuteInteraction(USocialUser& User) { User.InviteToParty(IOnlinePartySystem::GetPrimaryPartyTypeId()); } @@ -41,7 +38,7 @@ void FSocialInteraction_InviteToParty::ExecuteAction(ESocialSubsystem SocialSubs // JoinParty ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_JoinParty::GetDisplayName() +FText FSocialInteraction_JoinParty::GetDisplayName(const USocialUser& User) { return LOCTEXT("JoinParty", "Join Party"); } @@ -51,16 +48,13 @@ FString FSocialInteraction_JoinParty::GetSlashCommandToken() return TEXT("join"); } -void FSocialInteraction_JoinParty::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_JoinParty::CanExecute(const USocialUser& User) { - FJoinPartyResult Result; - if (User.CanJoinParty(IOnlinePartySystem::GetPrimaryPartyTypeId(), Result) && !User.HasSentPartyInvite()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + FJoinPartyResult MockJoinResult = User.CheckPartyJoinability(IOnlinePartySystem::GetPrimaryPartyTypeId()); + return MockJoinResult.WasSuccessful(); } -void FSocialInteraction_JoinParty::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_JoinParty::ExecuteInteraction(USocialUser& User) { User.JoinParty(IOnlinePartySystem::GetPrimaryPartyTypeId()); } @@ -69,7 +63,7 @@ void FSocialInteraction_JoinParty::ExecuteAction(ESocialSubsystem SocialSubsyste // AcceptPartyInvite ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_AcceptPartyInvite::GetDisplayName() +FText FSocialInteraction_AcceptPartyInvite::GetDisplayName(const USocialUser& User) { return LOCTEXT("AcceptPartyInvite", "Accept"); } @@ -79,15 +73,12 @@ FString FSocialInteraction_AcceptPartyInvite::GetSlashCommandToken() return TEXT("acceptpartyinvite"); } -void FSocialInteraction_AcceptPartyInvite::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_AcceptPartyInvite::CanExecute(const USocialUser& User) { - if (User.HasSentPartyInvite()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return User.HasSentPartyInvite(IOnlinePartySystem::GetPrimaryPartyTypeId()); } -void FSocialInteraction_AcceptPartyInvite::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_AcceptPartyInvite::ExecuteInteraction(USocialUser& User) { User.JoinParty(IOnlinePartySystem::GetPrimaryPartyTypeId()); } @@ -96,7 +87,7 @@ void FSocialInteraction_AcceptPartyInvite::ExecuteAction(ESocialSubsystem Social // RejectPartyInvite ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_RejectPartyInvite::GetDisplayName() +FText FSocialInteraction_RejectPartyInvite::GetDisplayName(const USocialUser& User) { return LOCTEXT("RejectPartyInvite", "Reject"); } @@ -106,24 +97,21 @@ FString FSocialInteraction_RejectPartyInvite::GetSlashCommandToken() return TEXT("rejectpartyinvite"); } -void FSocialInteraction_RejectPartyInvite::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_RejectPartyInvite::CanExecute(const USocialUser& User) { - if (User.HasSentPartyInvite()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return User.HasSentPartyInvite(IOnlinePartySystem::GetPrimaryPartyTypeId()); } -void FSocialInteraction_RejectPartyInvite::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_RejectPartyInvite::ExecuteInteraction(USocialUser& User) { - User.RejectPartyInvite(); + User.RejectPartyInvite(IOnlinePartySystem::GetPrimaryPartyTypeId()); } ////////////////////////////////////////////////////////////////////////// // LeaveParty ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_LeaveParty::GetDisplayName() +FText FSocialInteraction_LeaveParty::GetDisplayName(const USocialUser& User) { return LOCTEXT("LeaveParty", "Leave Party"); } @@ -133,19 +121,17 @@ FString FSocialInteraction_LeaveParty::GetSlashCommandToken() return TEXT("leave"); } -void FSocialInteraction_LeaveParty::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_LeaveParty::CanExecute(const USocialUser& User) { if (User.IsLocalUser()) { const UPartyMember* LocalMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId()); - if (LocalMember && LocalMember->GetParty().GetNumPartyMembers() > 1) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return LocalMember && LocalMember->GetParty().GetNumPartyMembers() > 1; } + return false; } -void FSocialInteraction_LeaveParty::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_LeaveParty::ExecuteInteraction(USocialUser& User) { if (const UPartyMember* LocalMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId())) { @@ -157,7 +143,7 @@ void FSocialInteraction_LeaveParty::ExecuteAction(ESocialSubsystem SocialSubsyst // KickPartyMember ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_KickPartyMember::GetDisplayName() +FText FSocialInteraction_KickPartyMember::GetDisplayName(const USocialUser& User) { return LOCTEXT("KickPartyMember", "Kick"); } @@ -167,16 +153,13 @@ FString FSocialInteraction_KickPartyMember::GetSlashCommandToken() return TEXT("kick"); } -void FSocialInteraction_KickPartyMember::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_KickPartyMember::CanExecute(const USocialUser& User) { const UPartyMember* PartyMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId()); - if (PartyMember && PartyMember->CanKickFromParty()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return PartyMember && PartyMember->CanKickFromParty(); } -void FSocialInteraction_KickPartyMember::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_KickPartyMember::ExecuteInteraction(USocialUser& User) { if (UPartyMember* PartyMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId())) { @@ -188,7 +171,7 @@ void FSocialInteraction_KickPartyMember::ExecuteAction(ESocialSubsystem SocialSu // PromoteToPartyLeader ////////////////////////////////////////////////////////////////////////// -FText FSocialInteraction_PromoteToPartyLeader::GetDisplayName() +FText FSocialInteraction_PromoteToPartyLeader::GetDisplayName(const USocialUser& User) { return LOCTEXT("PromoteToPartyLeader", "Promote"); } @@ -198,16 +181,13 @@ FString FSocialInteraction_PromoteToPartyLeader::GetSlashCommandToken() return TEXT("promote"); } -void FSocialInteraction_PromoteToPartyLeader::GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) +bool FSocialInteraction_PromoteToPartyLeader::CanExecute(const USocialUser& User) { const UPartyMember* PartyMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId()); - if (PartyMember && PartyMember->CanPromoteToLeader()) - { - OutAvailableSubsystems.Add(ESocialSubsystem::Primary); - } + return PartyMember && PartyMember->CanPromoteToLeader(); } -void FSocialInteraction_PromoteToPartyLeader::ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) +void FSocialInteraction_PromoteToPartyLeader::ExecuteInteraction(USocialUser& User) { if (UPartyMember* PartyMember = User.GetPartyMember(IOnlinePartySystem::GetPrimaryPartyTypeId())) { diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/SocialInteractionHandle.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/SocialInteractionHandle.cpp new file mode 100644 index 000000000000..aabe6951661a --- /dev/null +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Interactions/SocialInteractionHandle.cpp @@ -0,0 +1,46 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "Interactions/SocialInteractionHandle.h" +#include "Interactions/SocialInteractionMacros.h" + +FSocialInteractionHandle::FSocialInteractionHandle(const ISocialInteractionWrapper& Wrapper) + : InteractionWrapper(&Wrapper) +{} + +bool FSocialInteractionHandle::IsValid() const +{ + return InteractionWrapper != nullptr; +} + +bool FSocialInteractionHandle::operator==(const FSocialInteractionHandle& Other) const +{ + return InteractionWrapper == Other.InteractionWrapper; +} + +FName FSocialInteractionHandle::GetInteractionName() const +{ + return InteractionWrapper ? InteractionWrapper->GetInteractionName() : NAME_None; +} + +FText FSocialInteractionHandle::GetDisplayName(const USocialUser& User) const +{ + return InteractionWrapper ? InteractionWrapper->GetDisplayName(User) : FText::GetEmpty(); +} + +FString FSocialInteractionHandle::GetSlashCommandToken() const +{ + return InteractionWrapper ? InteractionWrapper->GetSlashCommandToken() : FString(); +} + +bool FSocialInteractionHandle::IsAvailable(const USocialUser& User) const +{ + return InteractionWrapper ? InteractionWrapper->IsAvailable(User) : false; +} + +void FSocialInteractionHandle::ExecuteInteraction(USocialUser& User) const +{ + if (InteractionWrapper) + { + InteractionWrapper->ExecuteInteraction(User); + } +} \ No newline at end of file diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyMember.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyMember.cpp index b50011d45327..fc1d7dea95f4 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyMember.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyMember.cpp @@ -86,7 +86,7 @@ void UPartyMember::InitializePartyMember(const TSharedRef& I void UPartyMember::InitializeLocalMemberRepData() { - UE_LOG(LogParty, VeryVerbose, TEXT("Initializing rep data for local member [%s]"), *ToDebugString()); + UE_LOG(LogParty, Verbose, TEXT("Initializing rep data for local member [%s]"), *ToDebugString()); MemberDataReplicator->SetPlatform(IOnlineSubsystem::GetLocalPlatformName()); MemberDataReplicator->SetPlatformUniqueId(SocialUser->GetUserId(ESocialSubsystem::Platform)); @@ -226,7 +226,7 @@ void UPartyMember::FinishInitializing() InitializeLocalMemberRepData(); } - UE_LOG(LogParty, Verbose, TEXT("Member [%s] is now fully initialized."), *ToDebugString()); + UE_LOG(LogParty, Verbose, TEXT("PartyMember [%s] is now fully initialized."), *ToDebugString()); OnInitializationComplete().Broadcast(); OnInitializationComplete().Clear(); } @@ -248,6 +248,7 @@ void UPartyMember::OnRemovedFromPartyInternal(EMemberExitedReason ExitReason) void UPartyMember::HandleSocialUserInitialized(USocialUser& InitializedUser) { + UE_LOG(LogParty, VeryVerbose, TEXT("PartyMember [%s]'s underlying SocialUser has been initialized"), *ToDebugString()); if (bHasReceivedInitialData) { FinishInitializing(); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.cpp index 568644f7bbf1..cda16e090126 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.cpp @@ -28,12 +28,30 @@ static bool IsTencentPlatform() #if !UE_BUILD_SHIPPING +static int32 ForcePlatformSessionFindFailure = 0; +static FAutoConsoleVariableRef CVar_ForcePlatformSessionFindFailure( + TEXT("Party.PlatformSession.Find.ForceFail"), + ForcePlatformSessionFindFailure, + TEXT("Always fail to find platform sessions.\n") + TEXT("0: Do not force fail platform session finds (default).\n") + TEXT("1: Fail the find without attempting it.\n"), + ECVF_Cheat +); + +static float PlatformSessionFindDelay = 0.f; +static FAutoConsoleVariableRef CVar_PlatformSessionFindDelay( + TEXT("Party.PlatformSession.Find.Delay"), + PlatformSessionFindDelay, + TEXT("Simulated delay (in seconds) between beginning an attempt to find a platform session and actually making the call the OSS."), + ECVF_Cheat +); + static int32 ForcePlatformSessionCreationFailure = 0; static FAutoConsoleVariableRef CVar_ForcePlatformSessionCreationFailure( TEXT("Party.PlatformSession.Create.ForceFail"), ForcePlatformSessionCreationFailure, - TEXT("Always fail to create console sessions.\n") - TEXT("0: Do not force fail console session creates (default).\n") + TEXT("Always fail to create platform sessions.\n") + TEXT("0: Do not force fail platform session creates (default).\n") TEXT("1: Fail the create without attempting it.\n"), ECVF_Cheat ); @@ -50,10 +68,9 @@ static int32 ForcePlatformSessionJoinFailure = 0; static FAutoConsoleVariableRef CVar_ForcePlatformSessionJoinFailure( TEXT("Party.PlatformSession.Join.ForceFail"), ForcePlatformSessionJoinFailure, - TEXT("Always fail to join console sessions.\n") - TEXT("0: Do not force fail console session joins (default).\n") - TEXT("1: Use an invalid session id during the lookup step.\n") - TEXT("2: Simulate failing the join after the successful lookup.\n"), + TEXT("Always fail to join platform sessions.\n") + TEXT("0: Do not force fail platform session joins (default).\n") + TEXT("1: Force fail the join without attempting it.\n"), ECVF_Cheat ); @@ -167,6 +184,33 @@ bool FPartyPlatformSessionManager::FindSessionInternal(const FSessionId& Session const FUniqueNetIdRepl& LocalUserPlatformId = GetLocalUserPlatformId(); if (ensure(LocalUserPlatformId.IsValid())) { +#if !UE_BUILD_SHIPPING + float DelaySeconds = FMath::Max(0.f, PlatformSessionFindDelay); + if (DelaySeconds > 0.f || ForcePlatformSessionFindFailure != 0) + { + UE_LOG(LogParty, Warning, TEXT("PartyPlatformSessionMonitor adding artificial delay of %0.2fs to session find attempt"), DelaySeconds); + + TWeakPtr AsWeakPtr = SharedThis(this); + FTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda( + [AsWeakPtr, SessionId, SessionOwnerId, LocalUserPlatformId, OnAttemptComplete, this](float) + { + if (AsWeakPtr.IsValid()) + { + if (ForcePlatformSessionFindFailure != 0) + { + UE_LOG(LogParty, Warning, TEXT("Forcing session find failure")); + ProcessCompletedSessionSearch(SocialManager.GetFirstLocalUserNum(), false, FOnlineSessionSearchResult(), SessionId, SessionOwnerId, OnAttemptComplete); + } + else + { + GetSessionInterface()->FindSessionById(*LocalUserPlatformId, FUniqueNetIdString(SessionId), *LocalUserPlatformId, FOnSingleSessionResultCompleteDelegate::CreateSP(this, &FPartyPlatformSessionManager::HandleFindSessionByIdComplete, SessionId, SessionOwnerId, OnAttemptComplete)); + } + } + return false; // Don't retick + }), DelaySeconds); + return ForcePlatformSessionFindFailure != 0; + } +#endif // Always start by trying to find the session directly by ID const IOnlineSessionPtr& SessionInterface = GetSessionInterface(); return SessionInterface->FindSessionById(*LocalUserPlatformId, FUniqueNetIdString(SessionId), *LocalUserPlatformId, FOnSingleSessionResultCompleteDelegate::CreateSP(this, &FPartyPlatformSessionManager::HandleFindSessionByIdComplete, SessionId, SessionOwnerId, OnAttemptComplete)); @@ -209,7 +253,7 @@ void FPartyPlatformSessionManager::HandleFindSessionByIdComplete(int32 LocalUser } } -void FPartyPlatformSessionManager::ProcessCompletedSessionSearch(int32 LocalUserNum, bool bWasSuccessful, const FOnlineSessionSearchResult& FoundSession, const FSessionId& SessionId, const FUniqueNetIdRepl& SessionOwnerId, FOnFindSessionAttemptComplete& OnAttemptComplete) +void FPartyPlatformSessionManager::ProcessCompletedSessionSearch(int32 LocalUserNum, bool bWasSuccessful, const FOnlineSessionSearchResult& FoundSession, const FSessionId& SessionId, const FUniqueNetIdRepl& SessionOwnerId, const FOnFindSessionAttemptComplete& OnAttemptComplete) { #if PLATFORM_PS4 bHasAlreadyRequeriedPSNFriends = false; @@ -673,16 +717,8 @@ void FPartyPlatformSessionMonitor::HandleCreateSessionComplete(FName SessionName if (bWasSuccessful) { -#if PLATFORM_PS4 - // Need to queue an immediate update of the newly created session to PUT the ChangeableSessionData - QueuePlatformSessionUpdate(); -#endif - SessionInitTracker.CompleteStep(Step_CreateSession); - const FUniqueNetIdRepl LocalUserPlatformId = SessionManager->GetLocalUserPlatformId(); - SessionInterface->RegisterPlayer(SessionName, *LocalUserPlatformId, false); - if (MonitoredParty.IsValid()) { MonitoredParty->SetIsMissingPlatformSession(false); @@ -691,6 +727,17 @@ void FPartyPlatformSessionMonitor::HandleCreateSessionComplete(FName SessionName const FNamedOnlineSession* Session = SessionInterface->GetNamedSession(PartySessionName); const FSessionId PlatformSessionId = ensure(Session) ? Session->GetSessionIdStr() : FSessionId(); MonitoredParty->GetOwningLocalMember().GetMutableRepData().SetPlatformSessionId(PlatformSessionId); + +#if PLATFORM_PS4 + // Need to queue an immediate update of the newly created session to PUT the ChangeableSessionData + QueuePlatformSessionUpdate(); +#endif + + const FUniqueNetIdRepl LocalUserPlatformId = SessionManager->GetLocalUserPlatformId(); + if (LocalUserPlatformId.IsValid()) + { + SessionInterface->RegisterPlayer(SessionName, *LocalUserPlatformId, false); + } } OnSessionEstablished.ExecuteIfBound(); @@ -874,7 +921,6 @@ bool FPartyPlatformSessionMonitor::ConfigurePlatformSessionSettings(FOnlineSessi // Xbox needs this false for privacy of session on dashboard SessionSettings.bUsesPresence = false; #else - // Needed on PC for Tencent OSS, PS4 doesn't care about this setting at all SessionSettings.bUsesPresence = true; #endif SessionSettings.NumPublicConnections = 0; diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.h index b233d8bfb39f..ff09683bcaff 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/PartyPlatformSessionMonitor.h @@ -35,7 +35,7 @@ private: void CreateMonitor(USocialParty& Party); bool FindSessionInternal(const FSessionId& SessionId, const FUniqueNetIdRepl& SessionOwnerId, const FOnFindSessionAttemptComplete& OnAttemptComplete); - void ProcessCompletedSessionSearch(int32 LocalUserNum, bool bWasSuccessful, const FOnlineSessionSearchResult& FoundSession, const FSessionId& SessionId, const FUniqueNetIdRepl& SessionOwnerId, FOnFindSessionAttemptComplete& OnAttemptComplete); + void ProcessCompletedSessionSearch(int32 LocalUserNum, bool bWasSuccessful, const FOnlineSessionSearchResult& FoundSession, const FSessionId& SessionId, const FUniqueNetIdRepl& SessionOwnerId, const FOnFindSessionAttemptComplete& OnAttemptComplete); void HandleFindSessionByIdComplete(int32 LocalUserNum, bool bWasSuccessful, const FOnlineSessionSearchResult& FoundSession, FSessionId SessionId, FUniqueNetIdRepl SessionOwnerId, FOnFindSessionAttemptComplete OnAttemptComplete); void HandleFindFriendSessionsComplete(int32 LocalUserNum, bool bWasSuccessful, const TArray& FoundSessions, FSessionId SessionId, FUniqueNetIdRepl SessionOwnerId, FOnFindSessionAttemptComplete OnAttemptComplete); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/SocialParty.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/SocialParty.cpp index 4928518581a5..87cbc29b9f24 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/SocialParty.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/Party/SocialParty.cpp @@ -4,6 +4,7 @@ #include "Party/PartyMember.h" #include "Party/PartyPlatformSessionMonitor.h" +#include "SocialSettings.h" #include "SocialManager.h" #include "SocialToolkit.h" #include "User/SocialUser.h" @@ -139,6 +140,25 @@ ECrossplayPreference GetCrossplayPreferenceFromJoinData(const FOnlinePartyData& return ECrossplayPreference::NoSelection; } +FPartyJoinApproval USocialParty::EvaluateJIPRequest(const FUniqueNetId& PlayerId) const +{ + FPartyJoinApproval JoinApproval; + + JoinApproval.SetApprovalAction(EApprovalAction::Deny); + JoinApproval.SetDenialReason(EPartyJoinDenialReason::GameModeRestricted); + for (const UPartyMember* Member : GetPartyMembers()) + { + // Make sure we are already in the party. + if (Member->GetPrimaryNetId() == PlayerId) + { + JoinApproval.SetApprovalAction(EApprovalAction::EnqueueAndStartBeacon); + JoinApproval.SetDenialReason(EPartyJoinDenialReason::NoReason); + break; + } + } + return JoinApproval; +} + FPartyJoinApproval USocialParty::EvaluateJoinRequest(const FUniqueNetId& PlayerId, const FUserPlatform& Platform, const FOnlinePartyData& JoinData, bool bFromJoinRequest) const { FPartyJoinApproval JoinApproval; @@ -147,6 +167,10 @@ FPartyJoinApproval USocialParty::EvaluateJoinRequest(const FUniqueNetId& PlayerI { JoinApproval.SetDenialReason(EPartyJoinDenialReason::PartyFull); } + else if (GetOwningLocalMember().GetSocialUser().GetOnlineStatus() == EOnlinePresenceState::Away) + { + JoinApproval.SetDenialReason(EPartyJoinDenialReason::TargetUserAway); + } else { const ECrossplayPreference SenderCrossplayPreference = GetCrossplayPreferenceFromJoinData(JoinData); @@ -261,8 +285,7 @@ bool USocialParty::TryInviteUser(const USocialUser& UserToInvite) if (CanInviteUser(UserToInvite)) { - //@todo DanH parties: Needs to be (or at least used to be) a config-exposed variable (ideally hotfixable...?) #required - static const bool bPreferPlatformInvite = true; + static const bool bPreferPlatformInvite = USocialSettings::ShouldPreferPlatformInvites(); const FUniqueNetIdRepl UserPrimaryId = UserToInvite.GetUserId(ESocialSubsystem::Primary); const FUniqueNetIdRepl UserPlatformId = UserToInvite.GetUserId(ESocialSubsystem::Platform); @@ -369,11 +392,13 @@ void USocialParty::InitializePartyInternal() PartyInterface->AddOnPartyConfigChangedDelegate_Handle(FOnPartyConfigChangedDelegate::CreateUObject(this, &USocialParty::HandlePartyConfigChanged)); PartyInterface->AddOnPartyDataReceivedDelegate_Handle(FOnPartyDataReceivedDelegate::CreateUObject(this, &USocialParty::HandlePartyDataReceived)); PartyInterface->AddOnPartyJoinRequestReceivedDelegate_Handle(FOnPartyJoinRequestReceivedDelegate::CreateUObject(this, &USocialParty::HandlePartyJoinRequestReceived)); + PartyInterface->AddOnPartyJIPRequestReceivedDelegate_Handle(FOnPartyJIPRequestReceivedDelegate::CreateUObject(this, &USocialParty::HandlePartyJIPRequestReceived)); PartyInterface->AddOnQueryPartyJoinabilityReceivedDelegate_Handle(FOnQueryPartyJoinabilityReceivedDelegate::CreateUObject(this, &USocialParty::HandleJoinabilityQueryReceived)); PartyInterface->AddOnPartyExitedDelegate_Handle(FOnPartyExitedDelegate::CreateUObject(this, &USocialParty::HandlePartyLeft)); PartyInterface->AddOnPartyStateChangedDelegate_Handle(FOnPartyStateChangedDelegate::CreateUObject(this, &USocialParty::HandlePartyStateChanged)); PartyInterface->AddOnPartyMemberJoinedDelegate_Handle(FOnPartyMemberJoinedDelegate::CreateUObject(this, &USocialParty::HandlePartyMemberJoined)); + PartyInterface->AddOnPartyJIPDelegate_Handle(FOnPartyJIPDelegate::CreateUObject(this, &USocialParty::HandlePartyMemberJIP)); PartyInterface->AddOnPartyMemberDataReceivedDelegate_Handle(FOnPartyMemberDataReceivedDelegate::CreateUObject(this, &USocialParty::HandlePartyMemberDataReceived)); PartyInterface->AddOnPartyMemberPromotedDelegate_Handle(FOnPartyMemberPromotedDelegate::CreateUObject(this, &USocialParty::HandlePartyMemberPromoted)); PartyInterface->AddOnPartyMemberExitedDelegate_Handle(FOnPartyMemberExitedDelegate::CreateUObject(this, &USocialParty::HandlePartyMemberExited)); @@ -542,6 +567,7 @@ void USocialParty::HandlePartyJoinRequestReceived(const FUniqueNetId& LocalUserI PendingApproval.SenderId.SetUniqueNetId(SenderId.AsShared()); PendingApproval.Platform = MemberPlatform; PendingApproval.JoinData = JoinData.AsShared(); + PendingApproval.bIsJIPApproval = false; PendingApprovals.Enqueue(PendingApproval); if (!ReservationBeaconClient && JoinApproval.GetApprovalAction() == EApprovalAction::EnqueueAndStartBeacon) @@ -558,6 +584,56 @@ void USocialParty::HandlePartyJoinRequestReceived(const FUniqueNetId& LocalUserI } } +void USocialParty::HandlePartyJIPRequestReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& SenderId) +{ + if (!IsLocalPlayerPartyLeader() || PartyId != GetPartyId()) + { + return; + } + + IOnlinePartyPtr PartyInterface = Online::GetPartyInterfaceChecked(GetWorld()); + + FPartyJoinApproval JoinApproval = EvaluateJIPRequest(SenderId); + + if (JoinApproval.GetApprovalAction() == EApprovalAction::Enqueue || + JoinApproval.GetApprovalAction() == EApprovalAction::EnqueueAndStartBeacon) + { + + FUserPlatform MemberPlatform = FUserPlatform(); + for (const UPartyMember* Member : GetPartyMembers()) + { + if (Member->GetPrimaryNetId() == SenderId) + { + MemberPlatform = Member->GetRepData().GetPlatform(); + break; + } + } + + // Enqueue for a more opportune time + UE_LOG(LogParty, Verbose, TEXT("[%s] Enqueuing JIP approval request for %s"), *PartyId.ToString(), *SenderId.ToString()); + + FPendingMemberApproval PendingApproval; + PendingApproval.RecipientId.SetUniqueNetId(LocalUserId.AsShared()); + PendingApproval.SenderId.SetUniqueNetId(SenderId.AsShared()); + PendingApproval.Platform = MemberPlatform; + PendingApproval.bIsJIPApproval = true; + PendingApprovals.Enqueue(PendingApproval); + + if (!ReservationBeaconClient && JoinApproval.GetApprovalAction() == EApprovalAction::EnqueueAndStartBeacon) + { + ConnectToReservationBeacon(); + } + } + else + { + const bool bIsApproved = JoinApproval.CanJoin(); + UE_LOG(LogParty, Verbose, TEXT("[%s] Responding to approval request for %s with %s"), *PartyId.ToString(), *SenderId.ToString(), bIsApproved ? TEXT("approved") : TEXT("denied")); + + PartyInterface->ApproveJIPRequest(LocalUserId, PartyId, SenderId, bIsApproved, JoinApproval.GetDenialReason()); + } +} + + void USocialParty::HandleJoinabilityQueryReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& SenderId, const FString& Platform, const FOnlinePartyData& JoinData) { if (PartyId == GetPartyId()) @@ -628,6 +704,15 @@ void USocialParty::HandlePartyMemberJoined(const FUniqueNetId& LocalUserId, cons } } +void USocialParty::HandlePartyMemberJIP(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, bool Success) +{ + if (PartyId == GetPartyId()) + { + // We are allowed to join the party.. start the JIP flow. + OnPartyJIPApprovedEvent.Broadcast(PartyId, Success); + } +} + void USocialParty::HandlePartyMemberPromoted(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& NewLeaderId) { if (PartyId == GetPartyId()) @@ -897,7 +982,7 @@ void USocialParty::SetPartyMaxSize(int32 NewSize) { if (CurrentConfig.MaxMembers != NewSize) { - CurrentConfig.MaxMembers = FMath::Clamp(NewSize, 1, GetSocialManager().GetDefaultPartyMaxSize()); + CurrentConfig.MaxMembers = FMath::Clamp(NewSize, 1, USocialSettings::GetDefaultMaxPartySize()); UpdatePartyConfig(); } } @@ -1087,8 +1172,15 @@ void USocialParty::ConnectToReservationBeacon() Reservation.UniqueId = NextApproval.SenderId; Reservation.Platform = NextApproval.Platform; - const ECrossplayPreference CrossplayPreference = GetCrossplayPreferenceFromJoinData(*NextApproval.JoinData); - Reservation.bAllowCrossplay = (CrossplayPreference == ECrossplayPreference::OptedIn); + if (!NextApproval.bIsJIPApproval) + { + const ECrossplayPreference CrossplayPreference = GetCrossplayPreferenceFromJoinData(*NextApproval.JoinData); + Reservation.bAllowCrossplay = (CrossplayPreference == ECrossplayPreference::OptedIn); + } + else + { + Reservation.bAllowCrossplay = true; // THis will not matter since we are JIP, and the session already has crossplay set. + } TArray ReservationAsArray; ReservationAsArray.Add(Reservation); @@ -1116,7 +1208,14 @@ void USocialParty::RejectAllPendingJoinRequests() { PendingApprovals.Dequeue(PendingApproval); UE_LOG(LogParty, Verbose, TEXT("[%s] Responding to approval request for %s with denied"), *PartyId.ToString(), *PendingApproval.SenderId.ToString()); - PartyInterface->ApproveJoinRequest(*PendingApproval.RecipientId, PartyId, *PendingApproval.SenderId, false, (int32)EPartyJoinDenialReason::Busy); + if (PendingApproval.bIsJIPApproval) + { + PartyInterface->ApproveJIPRequest(*PendingApproval.RecipientId, PartyId, *PendingApproval.SenderId, false, (int32)EPartyJoinDenialReason::Busy); + } + else + { + PartyInterface->ApproveJoinRequest(*PendingApproval.RecipientId, PartyId, *PendingApproval.SenderId, false, (int32)EPartyJoinDenialReason::Busy); + } } } @@ -1129,6 +1228,18 @@ void USocialParty::HandleBeaconHostConnectionFailed() CleanupReservationBeacon(); } +APartyBeaconClient* USocialParty::CreateReservationBeaconClient() +{ + UWorld* World = GetWorld(); + check(World); + + // Clear out our cached net driver name, we're going to create a new one here + LastReservationBeaconClientNetDriverName = NAME_None; + ReservationBeaconClient = World->SpawnActor(ReservationBeaconClientClass); + + return ReservationBeaconClient; +} + void USocialParty::PumpApprovalQueue() { // Check if there are any more while we are connected @@ -1137,17 +1248,36 @@ void USocialParty::PumpApprovalQueue() { if (ensure(ReservationBeaconClient)) { - ECrossplayPreference CrossplayPreference = GetCrossplayPreferenceFromJoinData(*NextApproval.JoinData); - - FPlayerReservation NewPlayerRes; - NewPlayerRes.UniqueId = NextApproval.SenderId; - NewPlayerRes.Platform = NextApproval.Platform; - NewPlayerRes.bAllowCrossplay = (CrossplayPreference == ECrossplayPreference::OptedIn); + if (NextApproval.bIsJIPApproval == false) + { + // This is a request to join our party + ECrossplayPreference CrossplayPreference = GetCrossplayPreferenceFromJoinData(*NextApproval.JoinData); - TArray PlayersToAdd; - PlayersToAdd.Add(NewPlayerRes); + FPlayerReservation NewPlayerRes; + NewPlayerRes.UniqueId = NextApproval.SenderId; + NewPlayerRes.Platform = NextApproval.Platform; + NewPlayerRes.bAllowCrossplay = (CrossplayPreference == ECrossplayPreference::OptedIn); - ReservationBeaconClient->RequestReservationUpdate(GetPartyLeader()->GetPrimaryNetId(), PlayersToAdd); + TArray PlayersToAdd; + PlayersToAdd.Add(NewPlayerRes); + + ReservationBeaconClient->RequestReservationUpdate(GetPartyLeader()->GetPrimaryNetId(), PlayersToAdd); + } + else + { + // This is a request from a party member to join a JIP game. + FPlayerReservation NewPlayerRes; + NewPlayerRes.UniqueId = NextApproval.SenderId; + NewPlayerRes.Platform = NextApproval.Platform; + + // This doesn't matter, since the crossplay state of the match has already been set. + NewPlayerRes.bAllowCrossplay = true; + + TArray PlayersToAdd; + PlayersToAdd.Add(NewPlayerRes); + + ReservationBeaconClient->RequestReservationUpdate(GetPartyLeader()->GetPrimaryNetId(), PlayersToAdd); + } } else { @@ -1175,7 +1305,15 @@ void USocialParty::HandleReservationRequestComplete(EPartyReservationResult::Typ if (ensure(PendingApprovals.Dequeue(PendingApproval))) { IOnlinePartyPtr PartyInterface = Online::GetPartyInterfaceChecked(GetWorld()); - PartyInterface->ApproveJoinRequest(*PendingApproval.RecipientId, GetPartyId(), *PendingApproval.SenderId, bReservationApproved, DenialReason); + if (PendingApproval.bIsJIPApproval) + { + // This player is already in our party. ApproveJIPRequest + PartyInterface->ApproveJIPRequest(*PendingApproval.RecipientId, GetPartyId(), *PendingApproval.SenderId, bReservationApproved, DenialReason); + } + else + { + PartyInterface->ApproveJoinRequest(*PendingApproval.RecipientId, GetPartyId(), *PendingApproval.SenderId, bReservationApproved, DenialReason); + } } PumpApprovalQueue(); } diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialManager.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialManager.cpp index e1f72c4c9fa7..0dfc01bf4cfe 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialManager.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialManager.cpp @@ -2,6 +2,7 @@ #include "SocialManager.h" #include "SocialToolkit.h" +#include "SocialSettings.h" #include "Interactions/CoreInteractions.h" #include "Interactions/PartyInteractions.h" @@ -65,10 +66,6 @@ FString USocialManager::FJoinPartyAttempt::ToDebugString() const *PlatformSessionId); } -////////////////////////////////////////////////////////////////////////// -// FJoinPartyAttempt -////////////////////////////////////////////////////////////////////////// - const FName USocialManager::FJoinPartyAttempt::Step_FindPlatformSession = TEXT("FindPlatformSession"); const FName USocialManager::FJoinPartyAttempt::Step_QueryJoinability = TEXT("QueryJoinability"); const FName USocialManager::FJoinPartyAttempt::Step_LeaveCurrentParty = TEXT("LeaveCurrentParty"); @@ -80,7 +77,7 @@ const FName USocialManager::FJoinPartyAttempt::Step_DeferredPartyCreation = TEXT ////////////////////////////////////////////////////////////////////////// TArray USocialManager::DefaultSubsystems; -TArray USocialManager::RegisteredInteractions; +TArray USocialManager::RegisteredInteractions; TMap, TWeakObjectPtr> USocialManager::AllManagersByGameInstance; /*static*/bool USocialManager::IsSocialSubsystemEnabled(ESocialSubsystem SubsystemType) @@ -345,7 +342,7 @@ void USocialManager::CreateParty(const FOnlinePartyTypeId& PartyTypeId, const FP void USocialManager::CreatePersistentParty(const FOnCreatePartyAttemptComplete& OnCreatePartyComplete) { - UE_LOG(LogParty, VeryVerbose, TEXT("Attempting to create new persistent party")); + UE_LOG(LogParty, Log, TEXT("Attempting to create new persistent party")); // The persistent party starts off closed by default, and will update its config as desired after initializing FPartyConfiguration InitialPersistentPartyConfig; @@ -354,36 +351,32 @@ void USocialManager::CreatePersistentParty(const FOnCreatePartyAttemptComplete& InitialPersistentPartyConfig.bShouldRemoveOnDisconnection = true; InitialPersistentPartyConfig.PresencePermissions = PartySystemPermissions::EPermissionType::Noone; InitialPersistentPartyConfig.InvitePermissions = PartySystemPermissions::EPermissionType::Noone; - InitialPersistentPartyConfig.MaxMembers = GetDefaultPartyMaxSize(); + InitialPersistentPartyConfig.MaxMembers = USocialSettings::GetDefaultMaxPartySize(); CreateParty(IOnlinePartySystem::GetPrimaryPartyTypeId(), InitialPersistentPartyConfig, OnCreatePartyComplete); } -void USocialManager::RegisterSubclassInteraction(ISocialInteractionHandleRef NewInteraction) -{ - RegisteredInteractions.Add(NewInteraction); -} - void USocialManager::RegisterSocialInteractions() { // Register Party Interactions - RegisteredInteractions.Add(FSocialInteraction_JoinParty::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_InviteToParty::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_AcceptPartyInvite::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_RejectPartyInvite::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_PromoteToPartyLeader::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_KickPartyMember::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_LeaveParty::GetHandle()); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); // Register Core interactions - RegisteredInteractions.Add(FSocialInteraction_AddFriend::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_AcceptFriendInvite::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_RejectFriendInvite::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_PrivateMessage::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_RemoveFriend::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_Block::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_Unblock::GetHandle()); - RegisteredInteractions.Add(FSocialInteraction_ShowPlatformProfile::GetHandle()); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); + RegisterInteraction(); } FJoinPartyResult USocialManager::ValidateJoinAttempt(const FOnlinePartyTypeId& PartyTypeId) const @@ -425,6 +418,10 @@ FJoinPartyResult USocialManager::ValidateJoinTarget(const USocialUser& UserToJoi { return FPartyJoinDenialReason(EPartyJoinDenialReason::NotLoggedIn); } + else if (UserToJoin.GetOnlineStatus() == EOnlinePresenceState::Away) + { + return FPartyJoinDenialReason(EPartyJoinDenialReason::TargetUserAway); + } else if (UserToJoin.GetPartyMember(PartyTypeId)) { return EJoinPartyCompletionResult::AlreadyInParty; @@ -445,7 +442,7 @@ FJoinPartyResult USocialManager::ValidateJoinTarget(const USocialUser& UserToJoi else if (!JoinInfo->IsAcceptingMembers()) { FPartyJoinDenialReason DenialReason = JoinInfo->GetNotAcceptingReason(); - if (DenialReason.GetReason() != EPartyJoinDenialReason::PartyPrivate || !UserToJoin.HasSentPartyInvite()) + if (DenialReason.GetReason() != EPartyJoinDenialReason::PartyPrivate || !UserToJoin.HasSentPartyInvite(PartyTypeId)) { return DenialReason; } @@ -464,7 +461,7 @@ FJoinPartyResult USocialManager::ValidateJoinTarget(const USocialUser& UserToJoi { return FPartyJoinDenialReason(EPartyJoinDenialReason::TargetUserPlayingDifferentGame); } - else if (!UserToJoin.HasSentPartyInvite()) + else if (!UserToJoin.HasSentPartyInvite(PartyTypeId)) { if (!PlatformPresence->bIsJoinable) { @@ -984,16 +981,30 @@ void USocialManager::HandlePartyLeft(EMemberExitedReason Reason, USocialParty* L JoinedPartiesByTypeId.Remove(PartyTypeId); } - OnPartyLeftInternal(*LeftParty); + OnPartyLeftInternal(*LeftParty, Reason); LeftParty->MarkPendingKill(); if (FJoinPartyAttempt* JoinAttempt = JoinAttemptsByTypeId.Find(PartyTypeId)) { - // We're in the process of joining another party of the same type JoinAttempt->ActionTimeTracker.CompleteStep(FJoinPartyAttempt::Step_LeaveCurrentParty); - JoinPartyInternal(*JoinAttempt); + + // We're in the process of joining another party of the same type - do we know where we're heading yet? + if (JoinAttempt->JoinInfo.IsValid() || JoinAttempt->RejoinInfo.IsValid()) + { + // Join the new party immediately and early out + JoinPartyInternal(*JoinAttempt); + return; + } + else + { + // An attempt to join a party of this type has been initiated, but something/someone decided to leave the party before the attempt was ready to do so + // It's not worth accounting for the potential limbo that this could put us into, so just abort the join attempt and let the explicit leave action win + UE_LOG(LogParty, Verbose, TEXT("Finished leaving party [%s] before the current join attempt established join info. Cancelling join attempt."), *LeftParty->ToDebugString()); + FinishJoinPartyAttempt(*JoinAttempt, FJoinPartyResult(EPartyJoinDenialReason::JoinAttemptAborted)); + } } - else if (LeftParty->IsPersistentParty() && GetFirstLocalUserToolkit()->IsOwnerLoggedIn()) + + if (LeftParty->IsPersistentParty() && GetFirstLocalUserToolkit()->IsOwnerLoggedIn()) { UE_LOG(LogParty, Verbose, TEXT("Finished leaving persistent party without a join/rejoin target. Creating a new persistent party now.")); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialQuery.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialQuery.h index 4b40884fd17b..3d55571fde52 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialQuery.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialQuery.h @@ -36,7 +36,7 @@ public: // Intentionally not implemented here to catch errors at compile time static FName GetQueryId(); - void AddUserId(const QueryUserIdT& UserId, const FOnQueryComplete& QueryCompleteHandler) + virtual void AddUserId(const QueryUserIdT& UserId, const FOnQueryComplete& QueryCompleteHandler) { CompletionCallbacksByUserId.Add(UserId, QueryCompleteHandler); } @@ -88,12 +88,13 @@ public: bool HandleExecuteQueries(float) { - bool bTryAgain = false; - // Execute all pending queries - for (const auto& IdQueriesPair : CurrentQueriesById) + TArray>> AllQueries; + CurrentQueriesById.GenerateValueArray(AllQueries); + + for (const TArray>& Queries : AllQueries) { - for (const TSharedRef& Query : IdQueriesPair.Value) + for (const TSharedRef& Query : Queries) { if (!Query->HasExecuted()) { @@ -103,7 +104,7 @@ public: } TickExecuteHandle.Reset(); - return bTryAgain; + return false; } private: diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialSettings.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialSettings.cpp new file mode 100644 index 000000000000..db2997863b05 --- /dev/null +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialSettings.cpp @@ -0,0 +1,35 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "SocialSettings.h" +#include "SocialManager.h" + +USocialSettings::USocialSettings() +{ + // Switch is the only default supported OSS that does not itself support multiple environments + OssNamesWithEnvironmentIdPrefix.Add(SWITCH_SUBSYSTEM); +} + +FString USocialSettings::GetUniqueIdEnvironmentPrefix(ESocialSubsystem SubsystemType) +{ + const USocialSettings& SettingsCDO = *GetDefault(); + + // We don't need to worry about world specificity here for the OSS (both because there is no platform PIE and because we aren't accessing data that could differ if there was) + IOnlineSubsystem* OSS = USocialManager::GetSocialOss(nullptr, SubsystemType); + if (OSS && SettingsCDO.OssNamesWithEnvironmentIdPrefix.Contains(OSS->GetSubsystemName())) + { + return OSS->GetOnlineEnvironmentName() + TEXT("_"); + } + return FString(); +} + +bool USocialSettings::ShouldPreferPlatformInvites() +{ + const USocialSettings& SettingsCDO = *GetDefault(); + return SettingsCDO.bPreferPlatformInvites; +} + +int32 USocialSettings::GetDefaultMaxPartySize() +{ + const USocialSettings& SettingsCDO = *GetDefault(); + return SettingsCDO.DefaultMaxPartySize; +} diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialToolkit.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialToolkit.cpp index b0eea3ebdd41..640441dbccb6 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialToolkit.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/SocialToolkit.cpp @@ -3,6 +3,7 @@ #include "SocialToolkit.h" #include "SocialManager.h" #include "SocialQuery.h" +#include "SocialSettings.h" #include "User/SocialUser.h" #include "User/SocialUserList.h" #include "Chat/SocialChatManager.h" @@ -33,6 +34,14 @@ class FSocialQuery_MapExternalIds : public TSocialQuery::AddUserId(MappableIdStr, QueryCompleteHandler); + } + virtual void ExecuteQuery() override { FUniqueNetIdRepl LocalUserPrimaryId = Toolkit.IsValid() ? Toolkit->GetLocalUserNetId(ESocialSubsystem::Primary) : FUniqueNetIdRepl(); @@ -46,13 +55,13 @@ public: { bHasExecuted = true; - TArray UserIds; - CompletionCallbacksByUserId.GenerateKeyArray(UserIds); - UE_LOG(LogParty, Log, TEXT("FSocialQuery_MapExternalIds executing for [%d] users on subsystem [%s]"), UserIds.Num(), ToString(SubsystemType)); + TArray ExternalUserIds; + CompletionCallbacksByUserId.GenerateKeyArray(ExternalUserIds); + UE_LOG(LogParty, Log, TEXT("FSocialQuery_MapExternalIds executing for [%d] users on subsystem [%s]"), ExternalUserIds.Num(), ToString(SubsystemType)); const FString AuthType = IdentityInterface->GetAuthType().ToLower(); FExternalIdQueryOptions QueryOptions(AuthType, false); - PrimaryUserInterface->QueryExternalIdMappings(*LocalUserPrimaryId, QueryOptions, UserIds, IOnlineUser::FOnQueryExternalIdMappingsComplete::CreateSP(this, &FSocialQuery_MapExternalIds::HandleQueryExternalIdMappingsComplete)); + PrimaryUserInterface->QueryExternalIdMappings(*LocalUserPrimaryId, QueryOptions, ExternalUserIds, IOnlineUser::FOnQueryExternalIdMappingsComplete::CreateSP(this, &FSocialQuery_MapExternalIds::HandleQueryExternalIdMappingsComplete)); } } else @@ -172,24 +181,23 @@ int32 USocialToolkit::GetLocalUserNum() const return GetOwningLocalPlayer().GetControllerId(); } -EOnlinePresenceState::Type USocialToolkit::GetLocalUserOnlineState() +const FOnlineUserPresence* USocialToolkit::GetPresenceInfo(ESocialSubsystem SubsystemType) const { - if (IOnlineSubsystem* PrimaryOss = GetSocialOss(ESocialSubsystem::Primary)) + if (IOnlineSubsystem* Oss = GetSocialOss(SubsystemType)) { - IOnlinePresencePtr PresenceInterface = PrimaryOss->GetPresenceInterface(); - FUniqueNetIdRepl LocalUserId = GetLocalUserNetId(ESocialSubsystem::Primary); + IOnlinePresencePtr PresenceInterface = Oss->GetPresenceInterface(); + FUniqueNetIdRepl LocalUserId = GetLocalUserNetId(SubsystemType); if (PresenceInterface.IsValid() && LocalUserId.IsValid()) { TSharedPtr CurrentPresence; PresenceInterface->GetCachedPresence(*LocalUserId, CurrentPresence); - if (CurrentPresence.IsValid()) { - return CurrentPresence->Status.State; + return CurrentPresence.Get(); } } } - return EOnlinePresenceState::Offline; + return nullptr; } void USocialToolkit::SetLocalUserOnlineState(EOnlinePresenceState::Type OnlineState) @@ -324,8 +332,8 @@ void USocialToolkit::QueueUserDependentActionInternal(const FUniqueNetIdRepl& Su void USocialToolkit::HandleControllerIdChanged(int32 NewId, int32 OldId) { - const IOnlineIdentityPtr IdentityInterface = Online::GetIdentityInterface(GetWorld()); - if (ensure(IdentityInterface.IsValid())) + IOnlineSubsystem* PrimaryOss = GetSocialOss(ESocialSubsystem::Primary); + if (const IOnlineIdentityPtr IdentityInterface = PrimaryOss ? PrimaryOss->GetIdentityInterface() : nullptr) { IdentityInterface->ClearOnLoginCompleteDelegates(OldId, this); IdentityInterface->ClearOnLoginStatusChangedDelegates(OldId, this); @@ -388,9 +396,14 @@ void USocialToolkit::NotifySubsystemIdEstablished(USocialUser& SocialUser, ESoci } } -bool USocialToolkit::TrySendFriendInvite(const USocialUser& SocialUser, ESocialSubsystem SubsystemType) const +bool USocialToolkit::TrySendFriendInvite(USocialUser& SocialUser, ESocialSubsystem SubsystemType) const { - if (!SocialUser.IsFriend(SubsystemType)) + if (SocialUser.GetFriendInviteStatus(SubsystemType) == EInviteStatus::PendingOutbound) + { + OnFriendInviteSent().Broadcast(SocialUser, SubsystemType); + return true; + } + else if (!SocialUser.IsFriend(SubsystemType)) { IOnlineFriendsPtr FriendsInterface = Online::GetFriendsInterface(GetWorld(), USocialManager::GetSocialOssName(SubsystemType)); const FUniqueNetIdRepl SubsystemId = SocialUser.GetUserId(SubsystemType); @@ -399,7 +412,7 @@ bool USocialToolkit::TrySendFriendInvite(const USocialUser& SocialUser, ESocialS if (FriendsInterface && SubsystemId.IsValid() && !bIsFriendshipRestricted) { - return FriendsInterface->SendInvite(GetLocalUserNum(), *SubsystemId, FriendListToQuery, FOnSendInviteComplete::CreateUObject(this, &USocialToolkit::HandleFriendInviteSent, SubsystemType)); + return FriendsInterface->SendInvite(GetLocalUserNum(), *SubsystemId, FriendListToQuery, FOnSendInviteComplete::CreateUObject(this, &USocialToolkit::HandleFriendInviteSent, SubsystemType, SocialUser.GetDisplayName())); } } return false; @@ -417,42 +430,36 @@ void USocialToolkit::OnOwnerLoggedIn() // Establish the owning player's ID on each subsystem and bind to events for general social goings-on const int32 LocalUserNum = GetLocalUserNum(); - for (ESocialSubsystem Subsystem : USocialManager::GetDefaultSubsystems()) + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) { - if (IOnlineSubsystem* OSS = GetSocialOss(Subsystem)) + FUniqueNetIdRepl LocalUserNetId = LocalUser->GetUserId(SubsystemType); + if (LocalUserNetId.IsValid()) { - IOnlineIdentityPtr IdentityInterface = OSS->GetIdentityInterface(); - FUniqueNetIdRepl LocalUserNetId = IdentityInterface.IsValid() ? IdentityInterface->GetUniquePlayerId(LocalUserNum) : nullptr; - if (LocalUserNetId.IsValid()) + IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); + check(OSS); + if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) { - OwnerIdsBySubsystem.FindOrAdd(Subsystem) = LocalUserNetId; + FriendsInterface->AddOnFriendRemovedDelegate_Handle(FOnFriendRemovedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendRemoved, SubsystemType)); + FriendsInterface->AddOnDeleteFriendCompleteDelegate_Handle(LocalUserNum, FOnDeleteFriendCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleDeleteFriendComplete, SubsystemType)); - if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) - { - FriendsInterface->AddOnFriendRemovedDelegate_Handle(FOnFriendRemovedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendRemoved, Subsystem)); - FriendsInterface->AddOnDeleteFriendCompleteDelegate_Handle(LocalUserNum, FOnDeleteFriendCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleDeleteFriendComplete, Subsystem)); + FriendsInterface->AddOnInviteReceivedDelegate_Handle(FOnInviteReceivedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteReceived, SubsystemType)); + FriendsInterface->AddOnInviteAcceptedDelegate_Handle(FOnInviteAcceptedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteAccepted, SubsystemType)); + FriendsInterface->AddOnInviteRejectedDelegate_Handle(FOnInviteRejectedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteRejected, SubsystemType)); - FriendsInterface->AddOnInviteReceivedDelegate_Handle(FOnInviteReceivedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteReceived, Subsystem)); - FriendsInterface->AddOnInviteAcceptedDelegate_Handle(FOnInviteAcceptedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteAccepted, Subsystem)); - FriendsInterface->AddOnInviteRejectedDelegate_Handle(FOnInviteRejectedDelegate::CreateUObject(this, &USocialToolkit::HandleFriendInviteRejected, Subsystem)); + FriendsInterface->AddOnBlockedPlayerCompleteDelegate_Handle(LocalUserNum, FOnBlockedPlayerCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleBlockPlayerComplete, SubsystemType)); + FriendsInterface->AddOnUnblockedPlayerCompleteDelegate_Handle(LocalUserNum, FOnUnblockedPlayerCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleUnblockPlayerComplete, SubsystemType)); - FriendsInterface->AddOnBlockedPlayerCompleteDelegate_Handle(LocalUserNum, FOnBlockedPlayerCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleBlockPlayerComplete, Subsystem)); - FriendsInterface->AddOnUnblockedPlayerCompleteDelegate_Handle(LocalUserNum, FOnUnblockedPlayerCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleUnblockPlayerComplete, Subsystem)); + FriendsInterface->AddOnRecentPlayersAddedDelegate_Handle(FOnRecentPlayersAddedDelegate::CreateUObject(this, &USocialToolkit::HandleRecentPlayersAdded, SubsystemType)); + } - FriendsInterface->AddOnRecentPlayersAddedDelegate_Handle(FOnRecentPlayersAddedDelegate::CreateUObject(this, &USocialToolkit::HandleRecentPlayersAdded, Subsystem)); - } + if (IOnlinePartyPtr PartyInterface = OSS->GetPartyInterface()) + { + PartyInterface->AddOnPartyInviteReceivedDelegate_Handle(FOnPartyInviteReceivedDelegate::CreateUObject(this, &USocialToolkit::HandlePartyInviteReceived)); + } - IOnlinePartyPtr PartyInterface = OSS->GetPartyInterface(); - if (PartyInterface.IsValid()) - { - PartyInterface->AddOnPartyInviteReceivedDelegate_Handle(FOnPartyInviteReceivedDelegate::CreateUObject(this, &USocialToolkit::HandlePartyInviteReceived)); - } - - IOnlinePresencePtr PresenceInterface = OSS->GetPresenceInterface(); - if (PresenceInterface.IsValid()) - { - PresenceInterface->AddOnPresenceReceivedDelegate_Handle(FOnPresenceReceivedDelegate::CreateUObject(this, &USocialToolkit::HandlePresenceReceived, Subsystem)); - } + if (IOnlinePresencePtr PresenceInterface = OSS->GetPresenceInterface()) + { + PresenceInterface->AddOnPresenceReceivedDelegate_Handle(FOnPresenceReceivedDelegate::CreateUObject(this, &USocialToolkit::HandlePresenceReceived, SubsystemType)); } } } @@ -477,13 +484,10 @@ void USocialToolkit::OnOwnerLoggedOut() UE_LOG(LogParty, Log, TEXT("LocalPlayer [%d] has logged out - wiping user roster from SocialToolkit."), GetLocalUserNum()); const int32 LocalUserNum = GetLocalUserNum(); - for (const auto& SubsystemIdPair : OwnerIdsBySubsystem) + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) { - if (IOnlineSubsystem* OSS = GetSocialOss(SubsystemIdPair.Key)) + if (IOnlineSubsystem* OSS = GetSocialOss(SubsystemType)) { - IOnlineIdentityPtr IdentityInterface = OSS->GetIdentityInterface(); - check(IdentityInterface); - IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface(); if (FriendsInterface.IsValid()) { @@ -522,7 +526,6 @@ void USocialToolkit::OnOwnerLoggedOut() } UsersBySubsystemIds.Reset(); - OwnerIdsBySubsystem.Reset(); AllUsers.Reset(); // Remake a fresh uninitialized local user @@ -533,36 +536,42 @@ void USocialToolkit::OnOwnerLoggedOut() void USocialToolkit::QueryFriendsLists() { - for (const TPair& SubsystemOwnerIdPair : OwnerIdsBySubsystem) + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) { - const ESocialSubsystem SubsystemType = SubsystemOwnerIdPair.Key; - IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); - check(OSS); - - if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) + const FUniqueNetIdRepl LocalUserNetId = LocalUser->GetUserId(SubsystemType); + if (LocalUserNetId.IsValid()) { - FriendsInterface->ReadFriendsList(GetLocalUserNum(), FriendListToQuery, FOnReadFriendsListComplete::CreateUObject(this, &USocialToolkit::HandleReadFriendsListComplete, SubsystemType)); + IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); + check(OSS); + + if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) + { + FriendsInterface->ReadFriendsList(GetLocalUserNum(), FriendListToQuery, FOnReadFriendsListComplete::CreateUObject(this, &USocialToolkit::HandleReadFriendsListComplete, SubsystemType)); + } } } } void USocialToolkit::QueryBlockedPlayers() { - for (const TPair& SubsystemOwnerIdPair : OwnerIdsBySubsystem) + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) { - const ESocialSubsystem SubsystemType = SubsystemOwnerIdPair.Key; - IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); - check(OSS); - - if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) + const FUniqueNetIdRepl LocalUserSubsystemId = LocalUser->GetUserId(SubsystemType); + if (LocalUserSubsystemId.IsValid()) { - //@todo DanH Social: There is an inconsistency in OSS interfaces - some just return false for unimplemented features while others return false and trigger the callback - // Seems like they should return false if the feature isn't implemented and trigger the callback for failure if it is implemented and couldn't start - // As it is now, there are two ways to know if the call didn't succeed and zero ways to know if it ever could - FriendsInterface->AddOnQueryBlockedPlayersCompleteDelegate_Handle(FOnQueryBlockedPlayersCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleQueryBlockedPlayersComplete, SubsystemType)); - if (!FriendsInterface->QueryBlockedPlayers(*SubsystemOwnerIdPair.Value)) + IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); + check(OSS); + + if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) { - FriendsInterface->ClearOnQueryBlockedPlayersCompleteDelegates(this); + //@todo DanH Social: There is an inconsistency in OSS interfaces - some just return false for unimplemented features while others return false and trigger the callback + // Seems like they should return false if the feature isn't implemented and trigger the callback for failure if it is implemented and couldn't start + // As it is now, there are two ways to know if the call didn't succeed and zero ways to know if it ever could + FriendsInterface->AddOnQueryBlockedPlayersCompleteDelegate_Handle(FOnQueryBlockedPlayersCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleQueryBlockedPlayersComplete, SubsystemType)); + if (!FriendsInterface->QueryBlockedPlayers(*LocalUserSubsystemId)) + { + FriendsInterface->ClearOnQueryBlockedPlayersCompleteDelegates(this); + } } } } @@ -570,18 +579,21 @@ void USocialToolkit::QueryBlockedPlayers() void USocialToolkit::QueryRecentPlayers() { - for (const TPair& SubsystemOwnerIdPair : OwnerIdsBySubsystem) + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) { - const ESocialSubsystem SubsystemType = SubsystemOwnerIdPair.Key; - IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); - check(OSS); - - if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) + const FUniqueNetIdRepl LocalUserSubsystemId = LocalUser->GetUserId(SubsystemType); + if (LocalUserSubsystemId.IsValid()) { - FriendsInterface->AddOnQueryRecentPlayersCompleteDelegate_Handle(FOnQueryRecentPlayersCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleQueryRecentPlayersComplete, SubsystemType)); - if (RecentPlayerNamespaceToQuery.IsEmpty() || !FriendsInterface->QueryRecentPlayers(*SubsystemOwnerIdPair.Value, RecentPlayerNamespaceToQuery)) + IOnlineSubsystem* OSS = GetSocialOss(SubsystemType); + check(OSS); + + if (IOnlineFriendsPtr FriendsInterface = OSS->GetFriendsInterface()) { - FriendsInterface->ClearOnQueryRecentPlayersCompleteDelegates(this); + FriendsInterface->AddOnQueryRecentPlayersCompleteDelegate_Handle(FOnQueryRecentPlayersCompleteDelegate::CreateUObject(this, &USocialToolkit::HandleQueryRecentPlayersComplete, SubsystemType)); + if (RecentPlayerNamespaceToQuery.IsEmpty() || !FriendsInterface->QueryRecentPlayers(*LocalUserSubsystemId, RecentPlayerNamespaceToQuery)) + { + FriendsInterface->ClearOnQueryRecentPlayersCompleteDelegates(this); + } } } } @@ -593,8 +605,14 @@ void USocialToolkit::HandlePlayerLoginStatusChanged(int32 LocalUserNum, ELoginSt { if (NewStatus == ELoginStatus::LoggedIn) { + if (!ensure(AllUsers.Num() == 0)) + { + // Nobody told us we logged out! Handle it now just so we're fresh, but not good! + OnOwnerLoggedOut(); + } + AllUsers.Add(LocalUser); - LocalUser->Initialize(NewId.AsShared()); + LocalUser->InitLocalUser(); if (IsOwnerLoggedIn()) { @@ -711,21 +729,56 @@ void USocialToolkit::HandlePresenceReceived(const FUniqueNetId& UserId, const TS { UpdatedUser->NotifyPresenceChanged(SubsystemType); } + else if (SubsystemType == ESocialSubsystem::Platform) + { + FString ErrorString = TEXT("Platform presence received, but existing SocialUser could not be found.\n"); + ErrorString += TEXT("Incoming UserId is ") + UserId.ToString() + TEXT(", as a UniqueIdRepl it's ") + FUniqueNetIdRepl(UserId).ToString(); + + ErrorString += TEXT("Outputting all cached platform IDs and the corresponding user: \n") + UserId.ToString(); + for (auto IdUserPair : UsersBySubsystemIds) + { + if (IdUserPair.Key.GetType() != MCP_SUBSYSTEM) + { + ErrorString += FString::Printf(TEXT("\tUserId [%s]: SocialUser [%s]\n"), *IdUserPair.Key.ToString(), *IdUserPair.Value->ToDebugString()); + if (IdUserPair.Key == FUniqueNetIdRepl(UserId) || !ensure(*IdUserPair.Key != UserId)) + { + ErrorString += TEXT("\t\tAnd look at that, this one DOES actually match. The map has lied to us!!\n"); + } + } + } + + UE_LOG(LogParty, Error, TEXT("%s"), *ErrorString); + } } void USocialToolkit::HandleQueryPrimaryUserIdMappingComplete(bool bWasSuccessful, const FUniqueNetId& RequestingUserId, const FString& DisplayName, const FUniqueNetId& IdentifiedUserId, const FString& Error) { - if (IdentifiedUserId.IsValid() && RequestingUserId != IdentifiedUserId) + if (!IdentifiedUserId.IsValid()) { - QueueUserDependentActionInternal(IdentifiedUserId, ESocialSubsystem::Primary, - [this] (USocialUser& SocialUser) - { - TrySendFriendInvite(SocialUser, ESocialSubsystem::Primary); - }); + NotifyFriendInviteFailed(IdentifiedUserId, DisplayName, ESendFriendInviteFailureReason::NotFound); + } + else if (RequestingUserId == IdentifiedUserId) + { + NotifyFriendInviteFailed(IdentifiedUserId, DisplayName, ESendFriendInviteFailureReason::AddingSelfFail); } else { - NotifyFriendInviteFailed(IdentifiedUserId, Error); + QueueUserDependentActionInternal(IdentifiedUserId, ESocialSubsystem::Primary, + [this, DisplayName] (USocialUser& SocialUser) + { + if (SocialUser.IsBlocked()) + { + NotifyFriendInviteFailed(*SocialUser.GetUserId(ESocialSubsystem::Primary), DisplayName, ESendFriendInviteFailureReason::AddingBlockedFail); + } + else if (SocialUser.IsFriend(ESocialSubsystem::Primary)) + { + NotifyFriendInviteFailed(*SocialUser.GetUserId(ESocialSubsystem::Primary), DisplayName, ESendFriendInviteFailureReason::AlreadyFriends); + } + else + { + TrySendFriendInvite(SocialUser, ESocialSubsystem::Primary); + } + }); } } @@ -782,7 +835,7 @@ void USocialToolkit::HandleFriendInviteRejected(const FUniqueNetId& LocalUserId, } } -void USocialToolkit::HandleFriendInviteSent(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& InvitedUserId, const FString& ListName, const FString& ErrorStr, ESocialSubsystem SubsystemType) +void USocialToolkit::HandleFriendInviteSent(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& InvitedUserId, const FString& ListName, const FString& ErrorStr, ESocialSubsystem SubsystemType, FString DisplayName) { if (bWasSuccessful) { @@ -800,6 +853,10 @@ void USocialToolkit::HandleFriendInviteSent(int32 LocalUserNum, bool bWasSuccess } }); } + else + { + NotifyFriendInviteFailed(InvitedUserId, ErrorStr, ESendFriendInviteFailureReason::UnknownError, false); + } } void USocialToolkit::HandleFriendRemoved(const FUniqueNetId& LocalUserId, const FUniqueNetId& FriendId, ESocialSubsystem SubsystemType) diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/User/SocialUser.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/User/SocialUser.cpp index 148b14521b69..ed523e29c898 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/User/SocialUser.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Private/User/SocialUser.cpp @@ -1,12 +1,12 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "User/SocialUser.h" -#include "SocialQuery.h" +#include "SocialQuery.h" +#include "SocialSettings.h" #include "SocialToolkit.h" #include "SocialManager.h" #include "User/ISocialUserList.h" -#include "Interactions/CoreInteractions.h" #include "Party/SocialParty.h" #include "Party/PartyMember.h" @@ -87,23 +87,14 @@ private: for (const TSharedRef& UserId : UserIds) { TSharedPtr UserInfo = UserInterface->GetUserInfo(LocalUserNum, *UserId); - //@todo DanH: TSharedRef is no good as a key here (addresses can be different), need to use the fancier TUniqueNetIdMap #suggested - /*if (FOnQueryComplete* Callback = CompletionCallbacksByUserId.Find(UserId)) + for (auto& IdCallbackPair : CompletionCallbacksByUserId) { - Callback->ExecuteIfBound(SubsystemType, bWasSuccessful, UserInfo); - } - else - {*/ - // TEMP - workaround for the hash issue mentioned above - for (auto& IdCallbackPair : CompletionCallbacksByUserId) + if (*UserId == *IdCallbackPair.Key) { - if (*UserId == *IdCallbackPair.Key) - { - IdCallbackPair.Value.ExecuteIfBound(SubsystemType, bWasSuccessful, UserInfo); - break; - } + IdCallbackPair.Value.ExecuteIfBound(SubsystemType, bWasSuccessful, UserInfo); + break; } - //} + } } OnQueryCompleted.ExecuteIfBound(GetQueryId(), AsShared()); @@ -134,6 +125,33 @@ TMap, FOnNewSocialUserInitialized> USocialUser::Init USocialUser::USocialUser() {} +void USocialUser::InitLocalUser() +{ + check(IsLocalUser()); + + USocialToolkit& OwningToolkit = GetOwningToolkit(); + UE_LOG(LogParty, Log, TEXT("Initializing local SocialUser for Toolkit [%d]"), OwningToolkit.GetLocalUserNum()); + + for (ESocialSubsystem SubsystemType : USocialManager::GetDefaultSubsystems()) + { + IOnlineSubsystem* OSS = OwningToolkit.GetSocialOss(SubsystemType); + check(OSS); + + IOnlineIdentityPtr IdentityInterface = OSS->GetIdentityInterface(); + FUniqueNetIdRepl LocalUserSubsystemId = IdentityInterface ? IdentityInterface->GetUniquePlayerId(OwningToolkit.GetLocalUserNum()) : FUniqueNetIdRepl(); + if (ensure(LocalUserSubsystemId.IsValid())) + { + SetSubsystemId(SubsystemType, LocalUserSubsystemId); + } + else + { + UE_LOG(LogParty, Error, TEXT("Local SocialUser unable to establish a valid UniqueId on subsystem [%s]"), ToString(SubsystemType)); + } + } + + TryBroadcastInitializationComplete(); +} + void USocialUser::Initialize(const FUniqueNetIdRepl& PrimaryId) { check(PrimaryId.IsValid()); @@ -194,46 +212,50 @@ TArray USocialUser::GetRelationshipSubsystems(ESocialRelations static TArray RelationshipSubsystems; RelationshipSubsystems.Reset(); - for (const TPair& SubsystemInfoPair : SubsystemInfoByType) + if (Relationship == ESocialRelationship::PartyInvite) { - switch (Relationship) + if (HasSentPartyInvite(IOnlinePartySystem::GetPrimaryPartyTypeId())) { - case ESocialRelationship::FriendInviteReceived: - if (SubsystemInfoPair.Value.GetFriendInviteStatus() == EInviteStatus::PendingInbound) + RelationshipSubsystems.Add(ESocialSubsystem::Primary); + } + } + else + { + for (const TPair& SubsystemInfoPair : SubsystemInfoByType) + { + switch (Relationship) { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); + case ESocialRelationship::FriendInviteReceived: + if (SubsystemInfoPair.Value.GetFriendInviteStatus() == EInviteStatus::PendingInbound) + { + RelationshipSubsystems.Add(SubsystemInfoPair.Key); + } + break; + case ESocialRelationship::FriendInviteSent: + if (SubsystemInfoPair.Value.GetFriendInviteStatus() == EInviteStatus::PendingOutbound) + { + RelationshipSubsystems.Add(SubsystemInfoPair.Key); + } + break; + case ESocialRelationship::Friend: + if (SubsystemInfoPair.Value.IsFriend()) + { + RelationshipSubsystems.Add(SubsystemInfoPair.Key); + } + break; + case ESocialRelationship::BlockedPlayer: + if (SubsystemInfoPair.Value.IsBlocked()) + { + RelationshipSubsystems.Add(SubsystemInfoPair.Key); + } + break; + case ESocialRelationship::RecentPlayer: + if (SubsystemInfoPair.Value.RecentPlayerInfo.IsValid() && !IsFriend()) + { + RelationshipSubsystems.Add(SubsystemInfoPair.Key); + } + break; } - break; - case ESocialRelationship::FriendInviteSent: - if (SubsystemInfoPair.Value.GetFriendInviteStatus() == EInviteStatus::PendingOutbound) - { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); - } - break; - case ESocialRelationship::PartyInvite: - if (HasSentPartyInvite()) - { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); - } - break; - case ESocialRelationship::Friend: - if (SubsystemInfoPair.Value.IsFriend()) - { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); - } - break; - case ESocialRelationship::BlockedPlayer: - if (SubsystemInfoPair.Value.IsBlocked()) - { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); - } - break; - case ESocialRelationship::RecentPlayer: - if (SubsystemInfoPair.Value.RecentPlayerInfo.IsValid() && !IsFriend()) - { - RelationshipSubsystems.Add(SubsystemInfoPair.Key); - } - break; } } @@ -257,6 +279,17 @@ USocialToolkit& USocialUser::GetOwningToolkit() const EOnlinePresenceState::Type USocialUser::GetOnlineStatus() const { + if (IsLocalUser()) + { + // FSubsystemUserInfo can only access presence on friends + // Use the Toolkit to read self presence + if (const FOnlineUserPresence* LocalPresenceInfo = GetOwningToolkit().GetPresenceInfo(ESocialSubsystem::Primary)) + { + return LocalPresenceInfo->Status.State; + } + return EOnlinePresenceState::Offline; + } + EOnlinePresenceState::Type OnlineStatus = EOnlinePresenceState::Offline; // Get the most "present" status available on any of the associated platforms @@ -264,12 +297,8 @@ EOnlinePresenceState::Type USocialUser::GetOnlineStatus() const { if (const FOnlineUserPresence* PresenceInfo = SubsystemInfoPair.Value.GetPresenceInfo()) { - if (PresenceInfo->bIsOnline) - { - OnlineStatus = EOnlinePresenceState::Online; - break; - } - else if (OnlineStatus == EOnlinePresenceState::Offline || PresenceInfo->Status.State == EOnlinePresenceState::Online || PresenceInfo->Status.State == EOnlinePresenceState::Away) + if (OnlineStatus == EOnlinePresenceState::Offline || PresenceInfo->Status.State == EOnlinePresenceState::Online || + (PresenceInfo->Status.State == EOnlinePresenceState::Away && OnlineStatus != EOnlinePresenceState::Online)) { // Either the best we have is offline, or the new one is either online or away (if necessary we can get into the weeds of prioritizing the other states) OnlineStatus = PresenceInfo->Status.State; @@ -286,7 +315,7 @@ void USocialUser::TryBroadcastInitializationComplete() { // We consider a social user to be initialized when it has valid primary OSS user info and no pending queries const FSubsystemUserInfo* SubsystemInfo = SubsystemInfoByType.Find(ESocialSubsystem::Primary); - if (SubsystemInfo && SubsystemInfo->UserInfo.IsValid()) + if (SubsystemInfo && ensureMsgf(SubsystemInfo->UserInfo.IsValid(), TEXT("SocialUser [%s] has primary subsystem info and no pending queries, but primary UserInfo is invalid!"), *ToDebugString())) { UE_LOG(LogParty, VeryVerbose, TEXT("SocialUser [%s] fully initialized."), *ToDebugString()); @@ -379,6 +408,12 @@ bool USocialUser::IsFriend() const return false; } +bool USocialUser::IsFriendshipPending(ESocialSubsystem SubsystemType) const +{ + const EInviteStatus::Type FriendInviteStatus = GetFriendInviteStatus(SubsystemType); + return FriendInviteStatus == EInviteStatus::PendingInbound || FriendInviteStatus == EInviteStatus::PendingOutbound; +} + const FOnlineUserPresence* USocialUser::GetFriendPresenceInfo(ESocialSubsystem SubsystemType) const { const FSubsystemUserInfo* SubsystemInfo = SubsystemInfoByType.Find(SubsystemType); @@ -386,6 +421,18 @@ const FOnlineUserPresence* USocialUser::GetFriendPresenceInfo(ESocialSubsystem S { return PresenceInfo; } + else if (IsLocalUser()) + { + IOnlineSubsystem* SocialOss = GetOwningToolkit().GetSocialOss(SubsystemType); + if (IOnlinePresencePtr PresenceInterface = SocialOss ? SocialOss->GetPresenceInterface() : nullptr) + { + TSharedPtr LocalUserPresence; + if (PresenceInterface->GetCachedPresence(*GetUserId(SubsystemType), LocalUserPresence) == EOnlineCachedResult::Success) + { + return LocalUserPresence.Get(); + } + } + } return nullptr; } @@ -482,7 +529,6 @@ void USocialUser::GetRichPresenceText(FText& OutRichPresence) const } else if (IsFriend()) { - //@todo DanH: Support the possibility of non-primary subsystems having rich presence const FOnlineUserPresence* PrimaryPresence = GetFriendPresenceInfo(ESocialSubsystem::Primary); if (PrimaryPresence && !PrimaryPresence->Status.StatusStr.IsEmpty()) { @@ -490,7 +536,15 @@ void USocialUser::GetRichPresenceText(FText& OutRichPresence) const } else { - OutRichPresence = EOnlinePresenceState::ToLocText(GetOnlineStatus()); + const FOnlineUserPresence* PlatformPresence = GetFriendPresenceInfo(ESocialSubsystem::Platform); + if (PlatformPresence && !PlatformPresence->Status.StatusStr.IsEmpty()) + { + OutRichPresence = FText::FromString(PlatformPresence->Status.StatusStr); + } + else + { + OutRichPresence = EOnlinePresenceState::ToLocText(GetOnlineStatus()); + } } } } @@ -563,24 +617,51 @@ bool USocialUser::GetUserAttribute(ESocialSubsystem SubsystemType, const FString return false; } -void USocialUser::GetAllAvailableInteractions(TMap>& OutInteractionsBySubsystem) const +bool USocialUser::HasAnyInteractionsAvailable() const { - static TArray InteractionSubsystems; - for (ISocialInteractionHandleRef SocialInteraction : USocialManager::GetRegisteredInteractions()) + for (const FSocialInteractionHandle& Interaction : USocialManager::GetRegisteredInteractions()) { - InteractionSubsystems.Reset(); - - SocialInteraction->GetAvailability(*this, InteractionSubsystems); - for (ESocialSubsystem InteractionSubsystem : InteractionSubsystems) + if (Interaction.IsAvailable(*this)) { - OutInteractionsBySubsystem.FindOrAdd(InteractionSubsystem).Add(SocialInteraction); + return true; } } + return false; +} + +TArray USocialUser::GetAllAvailableInteractions() const +{ + static TArray AvailableInteractions; + AvailableInteractions.Reset(); + + for (const FSocialInteractionHandle& Interaction : USocialManager::GetRegisteredInteractions()) + { + if (Interaction.IsAvailable(*this)) + { + AvailableInteractions.Add(Interaction); + } + } + return AvailableInteractions; +} + +bool USocialUser::CanSendFriendInvite(ESocialSubsystem SubsystemType) const +{ + if (SubsystemType == ESocialSubsystem::Platform) + { + //@todo DanH: Really need OssCaps or something to be able to just ask an OSS if it supports a given feature. For now, we just magically know that we only support sending XB, PSN, and WeGame invites + const FName PlatformOssName = USocialManager::GetSocialOssName(ESocialSubsystem::Platform); + if (PlatformOssName != LIVE_SUBSYSTEM && PlatformOssName != PS4_SUBSYSTEM && PlatformOssName != TENCENT_SUBSYSTEM) + { + return false; + } + } + + return HasSubsystemInfo(SubsystemType) && !IsFriend(SubsystemType) && !IsBlocked(SubsystemType) && !IsFriendshipPending(SubsystemType); } void USocialUser::JoinParty(const FOnlinePartyTypeId& PartyTypeId) const { - const bool bHasSentInvite = HasSentPartyInvite(); + const bool bHasSentInvite = HasSentPartyInvite(PartyTypeId); GetOwningToolkit().GetSocialManager().JoinParty(*this, PartyTypeId, USocialManager::FOnJoinPartyAttemptComplete()); @@ -593,9 +674,9 @@ void USocialUser::JoinParty(const FOnlinePartyTypeId& PartyTypeId) const } } -void USocialUser::RejectPartyInvite() +void USocialUser::RejectPartyInvite(const FOnlinePartyTypeId& PartyTypeId) { - if (HasSentPartyInvite()) + if (HasSentPartyInvite(PartyTypeId)) { IOnlinePartyPtr PartyInterface = Online::GetPartyInterfaceChecked(GetWorld()); PartyInterface->RejectInvitation(*GetOwningToolkit().GetLocalUserNetId(ESocialSubsystem::Primary), *GetUserId(ESocialSubsystem::Primary)); @@ -614,10 +695,12 @@ bool USocialUser::HasBeenInvitedToParty(const FOnlinePartyTypeId& PartyTypeId) c bool USocialUser::CanInviteToParty(const FOnlinePartyTypeId& PartyTypeId) const { - if (const USocialParty* Party = GetOwningToolkit().GetSocialManager().GetParty(PartyTypeId)) + if (!IsBlocked()) { - return Party->CanInviteUser(*this); + const USocialParty* Party = GetOwningToolkit().GetSocialManager().GetParty(PartyTypeId); + return Party && Party->CanInviteUser(*this); } + return false; } @@ -685,7 +768,7 @@ FString USocialUser::ToDebugString() const #endif } -bool USocialUser::SendFriendInvite(ESocialSubsystem SubsystemType) const +bool USocialUser::SendFriendInvite(ESocialSubsystem SubsystemType) { return GetOwningToolkit().TrySendFriendInvite(*this, SubsystemType); } @@ -726,10 +809,9 @@ bool USocialUser::EndFriendship(ESocialSubsystem SocialSubsystem) const return false; } -bool USocialUser::CanJoinParty(const FOnlinePartyTypeId& PartyTypeId, FJoinPartyResult& JoinResult) const +FJoinPartyResult USocialUser::CheckPartyJoinability(const FOnlinePartyTypeId& PartyTypeId) const { - JoinResult = GetOwningToolkit().GetSocialManager().ValidateJoinTarget(*this, PartyTypeId); - return JoinResult.WasSuccessful(); + return GetOwningToolkit().GetSocialManager().ValidateJoinTarget(*this, PartyTypeId); } bool USocialUser::ShowPlatformProfile() @@ -780,7 +862,7 @@ TSharedPtr USocialUser::GetPartyJoinInfo(const FOnli return nullptr; } -bool USocialUser::HasSentPartyInvite() const +bool USocialUser::HasSentPartyInvite(const FOnlinePartyTypeId& PartyTypeId) const { IOnlinePartyPtr PartyInterface = Online::GetPartyInterface(GetWorld()); if (PartyInterface.IsValid()) @@ -795,7 +877,7 @@ bool USocialUser::HasSentPartyInvite() const { for (const TSharedRef& InvitationJoinInfo : AllPendingInvites) { - if (*InvitationJoinInfo->GetSourceUserId() == *UserId) + if (*InvitationJoinInfo->GetSourceUserId() == *UserId && InvitationJoinInfo->GetPartyTypeId() == PartyTypeId) { return true; } @@ -901,6 +983,9 @@ void USocialUser::EstablishOssInfo(const TSharedRef& InFriendInfo *ToDebugString(), ToString(SubsystemType));*/ SubsystemInfo.FriendInfo = InFriendInfo; + + // Presence information on a user comes from the friend info, so if we have new friend info, we likely have wholly new presence info + OnPresenceChangedInternal(SubsystemType); } } @@ -988,8 +1073,15 @@ void USocialUser::SetUserInfo(ESocialSubsystem SubsystemType, const TSharedRefGetIdentityInterface()->GetAuthType()); FString SubsystemIdStr; - if (UserInfo->GetUserAttribute(SubsystemIdKey, SubsystemIdStr)) + if (UserInfo->GetUserAttribute(SubsystemIdKey, SubsystemIdStr) && !SubsystemIdStr.IsEmpty()) { + const FString IdPrefix = USocialSettings::GetUniqueIdEnvironmentPrefix(Subsystem); + if (!IdPrefix.IsEmpty()) + { + // Wipe the environment prefix from the stored ID string before converting it to a proper UniqueId + SubsystemIdStr.RemoveFromStart(IdPrefix); + } + FUniqueNetIdRepl SubsystemId = MissingOSS->GetIdentityInterface()->CreateUniquePlayerId(SubsystemIdStr); SetSubsystemId(Subsystem, SubsystemId); } @@ -1002,10 +1094,13 @@ void USocialUser::SetUserInfo(ESocialSubsystem SubsystemType, const TSharedRef& UserInfo) { --NumPendingQueries; - if (bWasSuccessful && UserInfo.IsValid()) + + if (UserInfo.IsValid()) { SetUserInfo(SubsystemType, UserInfo.ToSharedRef()); } + + UE_LOG(LogParty, VeryVerbose, TEXT("User [%s] finished querying user info on subsystem [%s] with result [%d]. [%d] queries still pending."), *ToDebugString(), ToString(SubsystemType), UserInfo.IsValid(), NumPendingQueries); TryBroadcastInitializationComplete(); } diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Chat/SocialChatChannel.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Chat/SocialChatChannel.h index 8eeaf57c9e2a..eb288424cafd 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Chat/SocialChatChannel.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Chat/SocialChatChannel.h @@ -81,12 +81,12 @@ public: // used by external classes to duplicate a message into a channel that didn't otherwise receive it void AddMirroredMessage(FSocialChatMessageRef NewMessage); + void AddSystemMessage(const FText& MessageBody); protected: IOnlineChatPtr GetChatInterface() const; void SanitizeMessage(FString& RawMessage) const; - void AddSystemMessage(const FString& MessageBody); void AddMessageInternal(FSocialChatMessageRef NewMessage); FText ChannelDisplayName; diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/CoreInteractions.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/CoreInteractions.h index 9d2ca1b5cad0..9d8ca3cedcdc 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/CoreInteractions.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/CoreInteractions.h @@ -8,7 +8,9 @@ */ DECLARE_SOCIAL_INTERACTION_EXPORT(PARTY_API, AddFriend); +DECLARE_SOCIAL_INTERACTION_EXPORT(PARTY_API, AddPlatformFriend); DECLARE_SOCIAL_INTERACTION_EXPORT(PARTY_API, RemoveFriend); + DECLARE_SOCIAL_INTERACTION_EXPORT(PARTY_API, AcceptFriendInvite); DECLARE_SOCIAL_INTERACTION_EXPORT(PARTY_API, RejectFriendInvite); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionHandle.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionHandle.h index cdf79f220fa5..41130d926d52 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionHandle.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionHandle.h @@ -1,39 +1,35 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #pragma once -#include "../SocialTypes.h" +#include "CoreMinimal.h" + +class USocialUser; +class ISocialInteractionWrapper; /** - * Represents a single discrete interaction between the local player and another user. - * Only necessary when you'd like to create some tangible list of interactions to iterate through. - * If, on the other hand, you're only interested in a few interactions, feel free to access their static APIs directly. + * Represents a single discrete interaction between a local player and another user. + * Useful for when you'd like to create some tangible list of interactions to compare/sort/classify/iterate. + * Not explicitly required if you have a particular known interaction in mind - feel free to access the static API of a given interaction directly. */ -class ISocialInteractionHandle : public TSharedFromThis +class PARTY_API FSocialInteractionHandle { public: - virtual ~ISocialInteractionHandle() {} + FSocialInteractionHandle() {} - virtual FString GetInteractionName() const = 0; - virtual FText GetDisplayName() const = 0; - virtual FString GetSlashCommandToken() const = 0; + bool IsValid() const; + bool operator==(const FSocialInteractionHandle& Other) const; + bool operator!=(const FSocialInteractionHandle& Other) const { return !operator==(Other); } - virtual void GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) const = 0; - virtual void ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) const = 0; -}; + FName GetInteractionName() const; + FText GetDisplayName(const USocialUser& User) const; + FString GetSlashCommandToken() const; -/** Link between the class-polymorphism-based interaction handle and the static template-polymorphism-based interactions */ -template -class TSocialInteractionHandle : public ISocialInteractionHandle -{ -public: - virtual FString GetInteractionName() const override final { return InteractionT::GetInteractionName(); } - virtual FText GetDisplayName() const override final { return InteractionT::GetDisplayName(); } - virtual FString GetSlashCommandToken() const override final { return InteractionT::GetSlashCommandToken(); } - - virtual void GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems) const override final { return InteractionT::GetAvailability(User, OutAvailableSubsystems); } - virtual void ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User) const override final { return InteractionT::ExecuteAction(SocialSubsystem, User); } + bool IsAvailable(const USocialUser& User) const; + void ExecuteInteraction(USocialUser& User) const; private: - friend InteractionT; - TSocialInteractionHandle() {} + template friend class TSocialInteractionWrapper; + FSocialInteractionHandle(const ISocialInteractionWrapper& Wrapper); + + const ISocialInteractionWrapper* InteractionWrapper = nullptr; }; \ No newline at end of file diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionMacros.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionMacros.h index 9acd9695f99c..755fbf89a17d 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionMacros.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Interactions/SocialInteractionMacros.h @@ -2,30 +2,83 @@ #pragma once -#include "SocialInteractionHandle.h" +#include "../SocialTypes.h" + +DECLARE_DELEGATE_RetVal_OneParam(bool, FOnCustomIsInteractionAvailable, const USocialUser&); + +/** + * Link between the class-polymorphism-based interaction handle and the static template-polymorphism-based interactions. + * Implementation detail, automatically set up and utilized in the DECLARE_SOCIAL_INTERACTION macros above + */ +class ISocialInteractionWrapper +{ +public: + virtual ~ISocialInteractionWrapper() {} + + virtual FName GetInteractionName() const = 0; + virtual FText GetDisplayName(const USocialUser& User) const = 0; + virtual FString GetSlashCommandToken() const = 0; + + virtual bool IsAvailable(const USocialUser& User) const = 0; + virtual void ExecuteInteraction(USocialUser& User) const = 0; +}; + +template +class TSocialInteractionWrapper : public ISocialInteractionWrapper +{ +public: + virtual FName GetInteractionName() const override final { return InteractionT::GetInteractionName(); } + virtual FText GetDisplayName(const USocialUser& User) const override final { return InteractionT::GetDisplayName(User); } + virtual FString GetSlashCommandToken() const override final { return InteractionT::GetSlashCommandToken(); } + + virtual bool IsAvailable(const USocialUser& User) const override final { return InteractionT::IsAvailable(User); } + virtual void ExecuteInteraction(USocialUser& User) const override final { InteractionT::ExecuteInteraction(User); } + +private: + friend InteractionT; + TSocialInteractionWrapper() {} + + FSocialInteractionHandle GetHandle() const { return FSocialInteractionHandle(*this); } +}; // Helper macros for declaring a social interaction class -// Not required to create one, but all static functions provided by the macro must be defined in order for a class to qualify as an interaction +// Establishes boilerplate behavior and declares all functions the user is required to provide #define DECLARE_SOCIAL_INTERACTION_EXPORT(APIMacro, InteractionName) \ class APIMacro FSocialInteraction_##InteractionName \ { \ public: \ - static const ISocialInteractionHandleRef GetHandle() \ + static FSocialInteractionHandle GetHandle() \ { \ - ISocialInteractionHandleRef MyHandle = MakeShareable(new TSocialInteractionHandle); \ - return MyHandle; \ + static const TSocialInteractionWrapper InteractionWrapper; \ + return InteractionWrapper.GetHandle(); \ } \ - static FString GetInteractionName() \ + static FName GetInteractionName() \ { \ return #InteractionName; \ } \ \ - static FText GetDisplayName(); \ + static FText GetDisplayName(const USocialUser& User); \ static FString GetSlashCommandToken(); \ \ - static void GetAvailability(const USocialUser& User, TArray& OutAvailableSubsystems); \ - static void ExecuteAction(ESocialSubsystem SocialSubsystem, USocialUser& User); \ + static bool IsAvailable(const USocialUser& User) \ + { \ + if (CanExecute(User)) \ + { \ + return OnCustomIsInteractionAvailable().IsBound() ? OnCustomIsInteractionAvailable().Execute(User) : true; \ + } \ + return false; \ + } \ + \ + static void ExecuteInteraction(USocialUser& User); \ + \ + static FOnCustomIsInteractionAvailable& OnCustomIsInteractionAvailable() \ + { \ + static FOnCustomIsInteractionAvailable CustomAvailabilityCheckDelegate; \ + return CustomAvailabilityCheckDelegate; \ + } \ +private: \ + static bool CanExecute(const USocialUser& User); \ } #define DECLARE_SOCIAL_INTERACTION(InteractionName) DECLARE_SOCIAL_INTERACTION_EXPORT(, InteractionName) \ No newline at end of file diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyDataReplicator.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyDataReplicator.h index b9dfbac0deff..c24de5b492e0 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyDataReplicator.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyDataReplicator.h @@ -7,7 +7,7 @@ #include "Containers/Ticker.h" #include "Interfaces/OnlinePartyInterface.h" -/** Util exclusively for use by TPartyDataReplicator to circumvent circular include header issues */ +/** Util exclusively for use by TPartyDataReplicator to circumvent circular include header issues (we can't include SocialParty.h or PartyMember.h here) */ class FPartyDataReplicatorHelper { template friend class TPartyDataReplicator; @@ -55,19 +55,23 @@ public: PACKAGE_SCOPE: void ProcessReceivedData(const FOnlinePartyData& IncomingPartyData, bool bCompareToPrevious = true) { - if (FVariantDataConverter::VariantMapToUStruct(IncomingPartyData.GetKeyValAttrs(), RepDataType, RepDataPtr, 0, CPF_Transient | CPF_RepSkip)) + // If the rep data can be edited locally, disregard any replication updates (they're the same at best or out of date at worst) + if (!static_cast(RepDataPtr)->CanEditData()) { - static_cast(RepDataPtr)->PostReplication(); - - if (bCompareToPrevious) + if (FVariantDataConverter::VariantMapToUStruct(IncomingPartyData.GetKeyValAttrs(), RepDataType, RepDataPtr, 0, CPF_Transient | CPF_RepSkip)) { - static_cast(RepDataPtr)->CompareAgainst(*RepDataCopy); + static_cast(RepDataPtr)->PostReplication(); + + if (bCompareToPrevious) + { + static_cast(RepDataPtr)->CompareAgainst(*RepDataCopy); + } + ensure(RepDataType->GetCppStructOps()->Copy(RepDataCopy, RepDataPtr, 1)); + } + else + { + UE_LOG(LogParty, Error, TEXT("Failed to serialize received party data!")); } - ensure(RepDataType->GetCppStructOps()->Copy(RepDataCopy, RepDataPtr, 1)); - } - else - { - UE_LOG(LogParty, Error, TEXT("Failed to serialize received party data!")); } } @@ -109,6 +113,9 @@ private: if (FVariantDataConverter::UStructToVariantMap(RepDataType, RepDataPtr, OnlinePartyData.GetKeyValAttrs(), 0, CPF_Transient | CPF_RepSkip)) { FPartyDataReplicatorHelper::ReplicateDataToMembers(*RepDataPtr, *RepDataType, OnlinePartyData); + + // Make sure the local copy lines up with whatever has been sent most recently + ensure(RepDataType->GetCppStructOps()->Copy(RepDataCopy, RepDataPtr, 1)); } return false; } diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyTypes.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyTypes.h index e3a551b9a1c9..5d46958ebc7d 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyTypes.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/PartyTypes.h @@ -43,6 +43,8 @@ enum class EPartyJoinDenialReason : uint8 /** No denial, matches success internally */ NoReason = 0, + /** The local player aborted the join attempt */ + JoinAttemptAborted, /** Party leader is busy or at inopportune time to allow joins - to be used as a fallback when there isn't a more specific reason (more specific reasons are preferred) */ Busy, /** Either the necessary OSS itself or critical element thereof (PartyInterface, SessionInterface, etc.) is missing. */ @@ -71,6 +73,8 @@ enum class EPartyJoinDenialReason : uint8 TargetUserMissingPresence, /** The target user's presence says the user is unjoinable */ TargetUserUnjoinable, + /** The target user is currently Away */ + TargetUserAway, /** We found ourself to be the leader of the friend's party according to the console session */ AlreadyLeaderInPlatformSession, /** The target user is not playing the same game as us */ @@ -83,7 +87,7 @@ enum class EPartyJoinDenialReason : uint8 FailedToStartFindConsoleSession, /** The party is of a type that the game does not support (it specified nullptr for the USocialParty class) */ MissingPartyClassForTypeId, - /**The target user is blocked by the local user on one or more of the active subsystems */ + /** The target user is blocked by the local user on one or more of the active subsystems */ TargetUserBlocked, /** @@ -142,6 +146,9 @@ inline const TCHAR* ToString(EPartyJoinDenialReason Type) case EPartyJoinDenialReason::NoReason: return TEXT("NoReason"); break; + case EPartyJoinDenialReason::JoinAttemptAborted: + return TEXT("JoinAttemptAborted"); + break; case EPartyJoinDenialReason::Busy: return TEXT("Busy"); break; diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/SocialParty.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/SocialParty.h index c29a6f762621..b62e3bd9cf07 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/SocialParty.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/Party/SocialParty.h @@ -175,6 +175,9 @@ public: DECLARE_EVENT_OneParam(USocialParty, FOnInviteSent, const USocialUser&); FOnInviteSent& OnInviteSent() const { return OnInviteSentEvent; } + DECLARE_EVENT_TwoParams(USocialParty, FOnPartyJIPApproved, const FOnlinePartyId&, bool /* Success*/); + FOnPartyJIPApproved& OnPartyJIPApproved() const { return OnPartyJIPApprovedEvent; } + const FPartyPrivacySettings& GetPrivacySettings() const; PARTY_SCOPE: @@ -217,6 +220,9 @@ protected: /** Determines the joinability of this party for a specific user requesting to join */ virtual FPartyJoinApproval EvaluateJoinRequest(const FUniqueNetId& PlayerId, const FUserPlatform& Platform, const FOnlinePartyData& JoinData, bool bFromJoinRequest) const; + /** Determines the joinability of the game a party is in for JoinInProgress */ + virtual FPartyJoinApproval EvaluateJIPRequest(const FUniqueNetId& PlayerId) const; + /** Determines the reason why, if at all, this party is currently flat-out unjoinable */ virtual FPartyJoinDenialReason DetermineCurrentJoinability() const; @@ -232,6 +238,8 @@ protected: */ void ConnectToReservationBeacon(); void CleanupReservationBeacon(); + APartyBeaconClient* CreateReservationBeaconClient(); + APartyBeaconClient* GetReservationBeaconClient() const { return ReservationBeaconClient; } /** Child classes MUST call EstablishRepDataInstance() on this using their member rep data struct instance */ @@ -265,10 +273,12 @@ private: // Handlers void HandlePartyDataReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const TSharedRef& PartyData); void HandleJoinabilityQueryReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& SenderId, const FString& Platform, const FOnlinePartyData& JoinData); void HandlePartyJoinRequestReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& SenderId, const FString& Platform, const FOnlinePartyData& JoinData); + void HandlePartyJIPRequestReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& SenderId); void HandlePartyLeft(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId); void HandlePartyMemberExited(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& MemberId, EMemberExitedReason ExitReason); void HandlePartyMemberDataReceived(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& MemberId, const TSharedRef& PartyMemberData); void HandlePartyMemberJoined(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& MemberId); + void HandlePartyMemberJIP(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, bool Success); void HandlePartyMemberPromoted(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& NewLeaderId); void HandlePartyPromotionLockoutChanged(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, bool bArePromotionsLocked); @@ -301,6 +311,7 @@ private: FUniqueNetIdRepl RecipientId; FUniqueNetIdRepl SenderId; FUserPlatform Platform; + bool bIsJIPApproval; TSharedPtr JoinData; }; TQueue PendingApprovals; @@ -336,4 +347,5 @@ private: mutable FOnPartyStateChanged OnPartyStateChangedEvent; mutable FOnPartyFunctionalityDegradedChanged OnPartyFunctionalityDegradedChangedEvent; mutable FOnInviteSent OnInviteSentEvent; + mutable FOnPartyJIPApproved OnPartyJIPApprovedEvent; }; diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialManager.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialManager.h index 71b9c280aec8..9ccd35055da0 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialManager.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialManager.h @@ -9,6 +9,7 @@ #include "Party/PartyTypes.h" #include "Interfaces/OnlinePartyInterface.h" +#include "Interactions/SocialInteractionHandle.h" #include "SocialManager.generated.h" @@ -23,56 +24,6 @@ class FPartyPlatformSessionManager; enum ETravelType; -/** - * Holds the basic information needed to join a party - */ -struct FPartyDetails : public TSharedFromThis -{ - FPartyDetails(const TSharedRef& InPartyJoinInfo, bool bInAcceptInvite = true) - : PartyJoinInfo(InPartyJoinInfo), bAcceptInvite(bInAcceptInvite) - {} - - virtual ~FPartyDetails() {} - - bool IsValid() const - { - return PartyJoinInfo->IsValid(); - } - - const TSharedRef& GetPartyId() const - { - return PartyJoinInfo->GetPartyId(); - } - - const FOnlinePartyTypeId GetPartyTypeId() const - { - return PartyJoinInfo->GetPartyTypeId(); - } - - const TSharedRef& GetSourceUserId() const - { - return PartyJoinInfo->GetSourceUserId(); - } - - const FString& GetAppId() const - { - return PartyJoinInfo->GetAppId(); - } - - virtual FString ToString() const - { - return FString::Printf( - TEXT("PartyId: %s SourceUserId: %s App: %s"), - *GetPartyId()->ToDebugString(), - *GetSourceUserId()->ToDebugString(), - *GetAppId()); - } - - TSharedRef PartyJoinInfo; - bool bAcceptInvite; -}; - - /** Singleton manager at the top of the social framework */ UCLASS(Within = GameInstance, Config = Game) class PARTY_API USocialManager : public UObject @@ -85,7 +36,7 @@ public: static IOnlineSubsystem* GetSocialOss(UWorld* World, ESocialSubsystem SubsystemType); static FUserPlatform GetLocalUserPlatform(); static const TArray& GetDefaultSubsystems() { return DefaultSubsystems; } - static const TArray& GetRegisteredInteractions() { return RegisteredInteractions; } + static const TArray& GetRegisteredInteractions() { return RegisteredInteractions; } USocialManager(); static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); @@ -130,8 +81,6 @@ public: return Cast(GetPartyInternal(PartyId)); } - int32 GetDefaultPartyMaxSize() const { return DefaultMaxPartySize; } - bool IsConnectedToPartyService() const { return bIsConnectedToPartyService; } PARTY_SCOPE: @@ -179,9 +128,6 @@ protected: FSocialActionTimeTracker ActionTimeTracker; }; - void RegisterSubclassInteraction(ISocialInteractionHandleRef NewInteraction); - - /** Register social interactions. */ virtual void RegisterSocialInteractions(); /** Validate that we are clear to try joining a party of the given type. If not, gives the reason why. */ @@ -199,7 +145,7 @@ protected: //virtual void OnQueryJoinabilityComplete(const FOnlinePartyId& PartyId, EJoinPartyCompletionResult Result, int32 DeniedResultCode, FOnlinePartyTypeId PartyTypeId) {} virtual void OnJoinPartyAttemptCompleteInternal(const FJoinPartyAttempt& JoinAttemptInfo, const FJoinPartyResult& Result); - virtual void OnPartyLeftInternal(USocialParty& LeftParty) {} + virtual void OnPartyLeftInternal(USocialParty& LeftParty, EMemberExitedReason Reason) {} virtual void OnToolkitCreatedInternal(USocialToolkit& NewToolkit); virtual bool CanCreateNewPartyObjects() const; @@ -209,6 +155,12 @@ protected: virtual bool ShouldTryRejoiningPersistentParty(const FRejoinableParty& InRejoinableParty) const; + template + void RegisterInteraction() + { + RegisteredInteractions.Add(InteractionT::GetHandle()); + } + void RefreshCanCreatePartyObjects(); USocialParty* GetPersistentPartyInternal(bool bEvenIfLeaving = false) const; @@ -223,9 +175,6 @@ protected: /** The desired type of SocialToolkit to create for each local player */ TSubclassOf ToolkitClass; - UPROPERTY(Config) - int32 DefaultMaxPartySize = 4; - private: UGameInstance& GetGameInstance() const; USocialToolkit& CreateSocialToolkit(ULocalPlayer& OwningLocalPlayer); @@ -264,7 +213,7 @@ private: // Handlers private: static TArray DefaultSubsystems; - static TArray RegisteredInteractions; + static TArray RegisteredInteractions; static TMap, TWeakObjectPtr> AllManagersByGameInstance; UPROPERTY() diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialSettings.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialSettings.h new file mode 100644 index 000000000000..f9d663e076fd --- /dev/null +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialSettings.h @@ -0,0 +1,43 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "UObject/Object.h" + +#include "SocialSettings.generated.h" + +enum class ESocialSubsystem : uint8; + +/** + * Config-driven settings object for the social framework. + * Only the CDO is ever expected to be used, no instance is ever expected to be created. + */ +UCLASS(Config = Game) +class PARTY_API USocialSettings : public UObject +{ + GENERATED_BODY() + +public: + USocialSettings(); + + static FString GetUniqueIdEnvironmentPrefix(ESocialSubsystem SubsystemType); + static bool ShouldPreferPlatformInvites(); + static int32 GetDefaultMaxPartySize(); + +private: + /** + * The specific OSS' that have their IDs stored with an additional prefix for the environment to which they pertain. + * This is only necessary for OSS' (ex: Switch) that do not have separate environments, just one big pot with both dev and prod users/friendships/etc. + * For these cases, the linked account ID stored on the Primary UserInfo for this particular OSS will be prefixed with the specific environment in which the linkage exists. + * Additionally, the prefix must be prepended when mapping the external ID to a primary ID. + * Overall, it's a major hassle that can hopefully be done away with eventually, but for now is necessary to fake environmental behavior on OSS' without environments. + */ + UPROPERTY(config) + TArray OssNamesWithEnvironmentIdPrefix; + + UPROPERTY(config) + int32 DefaultMaxPartySize = 4; + + UPROPERTY(config) + bool bPreferPlatformInvites = true; +}; \ No newline at end of file diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialToolkit.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialToolkit.h index 4de72c69416d..eed34c1d8772 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialToolkit.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialToolkit.h @@ -45,7 +45,7 @@ public: FUniqueNetIdRepl GetLocalUserNetId(ESocialSubsystem SubsystemType) const; int32 GetLocalUserNum() const; - EOnlinePresenceState::Type GetLocalUserOnlineState(); + const FOnlineUserPresence* GetPresenceInfo(ESocialSubsystem SubsystemType) const; void SetLocalUserOnlineState(EOnlinePresenceState::Type OnlineState); USocialManager& GetSocialManager() const; @@ -97,7 +97,7 @@ PARTY_SCOPE: void NotifySubsystemIdEstablished(USocialUser& SocialUser, ESocialSubsystem SubsystemType, const FUniqueNetIdRepl& SubsystemId); TSubclassOf GetChatManagerClass() { return ChatManagerClass; } - bool TrySendFriendInvite(const USocialUser& SocialUser, ESocialSubsystem SubsystemType) const; + bool TrySendFriendInvite(USocialUser& SocialUser, ESocialSubsystem SubsystemType) const; #if PLATFORM_PS4 void NotifyPSNFriendsListRebuilt(); @@ -110,7 +110,7 @@ protected: virtual void OnOwnerLoggedOut(); virtual void OnAllUsersInitialized() {} - virtual void NotifyFriendInviteFailed(const FUniqueNetId& InvitedUserId, const FString& ErrorStr) {} + virtual void NotifyFriendInviteFailed(const FUniqueNetId& InvitedUserId, const FString& InvitedUserName, ESendFriendInviteFailureReason FailureReason, bool bCanShow = true) {} void QueryFriendsLists(); void QueryBlockedPlayers(); @@ -172,7 +172,7 @@ private: // Handlers void HandleFriendInviteReceived(const FUniqueNetId& LocalUserId, const FUniqueNetId& SenderId, ESocialSubsystem SubsystemType); void HandleFriendInviteAccepted(const FUniqueNetId& LocalUserId, const FUniqueNetId& NewFriendId, ESocialSubsystem SubsystemType); void HandleFriendInviteRejected(const FUniqueNetId& LocalUserId, const FUniqueNetId& RejecterId, ESocialSubsystem SubsystemType); - void HandleFriendInviteSent(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& InvitedUserId, const FString& ListName, const FString& ErrorStr, ESocialSubsystem SubsystemType); + void HandleFriendInviteSent(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& InvitedUserId, const FString& ListName, const FString& ErrorStr, ESocialSubsystem SubsystemType, FString DisplayName); void HandleFriendRemoved(const FUniqueNetId& LocalUserId, const FUniqueNetId& FormerFriendId, ESocialSubsystem SubsystemType); void HandleDeleteFriendComplete(int32 LocalPlayer, bool bWasSuccessful, const FUniqueNetId& FormerFriendId, const FString& ListName, const FString& ErrorStr, ESocialSubsystem SubsystemType); @@ -190,8 +190,6 @@ private: // Handlers private: static TMap, TWeakObjectPtr> AllToolkitsByOwningPlayer; - TMap OwnerIdsBySubsystem; - UPROPERTY() USocialUser* LocalUser; diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialTypes.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialTypes.h index a0eb0131aba8..8a0ec113fd8e 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialTypes.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/SocialTypes.h @@ -6,6 +6,7 @@ #include "OnlineSubsystemTypes.h" #include "GameFramework/OnlineReplStructs.h" #include "Templates/SubclassOf.h" +#include "Interactions/SocialInteractionHandle.h" #include "SocialTypes.generated.h" @@ -65,6 +66,17 @@ enum class ECrossplayPreference : uint8 OptedOutRestricted }; +UENUM() +enum class ESendFriendInviteFailureReason : uint8 +{ + NotFound, + AlreadyFriends, + InvitePending, + AddingSelfFail, + AddingBlockedFail, + UnknownError +}; + /** Thin wrapper to infuse a raw platform string with some meaning */ USTRUCT() struct PARTY_API FUserPlatform @@ -142,7 +154,6 @@ using Type##PtrConst = TSharedPtr; \ using Type##Ref = TSharedRef; \ using Type##RefConst = TSharedRef -DECLARE_SHARED_PTR_ALIASES(ISocialInteractionHandle); DECLARE_SHARED_PTR_ALIASES(ISocialUserList); DECLARE_SHARED_PTR_ALIASES(FSocialChatMessage); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/ISocialUserList.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/ISocialUserList.h index f15701856bfc..3c8c39ffc811 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/ISocialUserList.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/ISocialUserList.h @@ -42,7 +42,7 @@ class ISocialUserList public: virtual ~ISocialUserList() {} - DECLARE_EVENT_OneParam(ISocialUserList, FOnUserAdded, const USocialUser&) + DECLARE_EVENT_OneParam(ISocialUserList, FOnUserAdded, USocialUser&) virtual FOnUserAdded& OnUserAdded() const = 0; DECLARE_EVENT_OneParam(ISocialUserList, FOnUserRemoved, const USocialUser&) diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/SocialUser.h b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/SocialUser.h index e0d025887837..6dd8fb8ce2de 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/SocialUser.h +++ b/Engine/Plugins/Online/OnlineFramework/Source/Party/Public/User/SocialUser.h @@ -2,10 +2,11 @@ #pragma once +#include "../SocialTypes.h" #include "UObject/Object.h" #include "OnlineSessionSettings.h" -#include "../SocialTypes.h" -#include "../Party/PartyTypes.h" +#include "Party/PartyTypes.h" +#include "Interactions/SocialInteractionHandle.h" #include "SocialUser.generated.h" @@ -42,9 +43,11 @@ public: FUniqueNetIdRepl GetUserId(ESocialSubsystem SubsystemType) const; FString GetDisplayName() const; FString GetDisplayName(ESocialSubsystem SubsystemType) const; + EInviteStatus::Type GetFriendInviteStatus(ESocialSubsystem SubsystemType) const; bool IsFriend() const; bool IsFriend(ESocialSubsystem SubsystemType) const; + bool IsFriendshipPending(ESocialSubsystem SubsystemType) const; const FOnlineUserPresence* GetFriendPresenceInfo(ESocialSubsystem SubsystemType) const; FDateTime GetFriendshipCreationDate() const; FText GetSocialName() const; @@ -61,9 +64,11 @@ public: bool SetUserLocalAttribute(ESocialSubsystem SubsystemType, const FString& AttrName, const FString& AttrValue); bool GetUserAttribute(ESocialSubsystem SubsystemType, const FString& AttrName, FString& OutAttrValue) const; - void GetAllAvailableInteractions(TMap>& OutInteractionsBySubsystem) const; + bool HasAnyInteractionsAvailable() const; + TArray GetAllAvailableInteractions() const; - bool SendFriendInvite(ESocialSubsystem SubsystemType) const; + bool CanSendFriendInvite(ESocialSubsystem SubsystemType) const; + bool SendFriendInvite(ESocialSubsystem SubsystemType); bool AcceptFriendInvite(ESocialSubsystem SocialSubsystem) const; bool RejectFriendInvite(ESocialSubsystem SocialSubsystem) const; bool EndFriendship(ESocialSubsystem SocialSubsystem) const; @@ -72,10 +77,10 @@ public: TSharedPtr GetPartyJoinInfo(const FOnlinePartyTypeId& PartyTypeId) const; - bool HasSentPartyInvite() const; - virtual bool CanJoinParty(const FOnlinePartyTypeId& PartyTypeId, FJoinPartyResult& JoinResult) const; + bool HasSentPartyInvite(const FOnlinePartyTypeId& PartyTypeId) const; + FJoinPartyResult CheckPartyJoinability(const FOnlinePartyTypeId& PartyTypeId) const; void JoinParty(const FOnlinePartyTypeId& PartyTypeId) const; - void RejectPartyInvite(); + void RejectPartyInvite(const FOnlinePartyTypeId& PartyTypeId); bool HasBeenInvitedToParty(const FOnlinePartyTypeId& PartyTypeId) const; bool CanInviteToParty(const FOnlinePartyTypeId& PartyTypeId) const; @@ -108,6 +113,7 @@ public: FString ToDebugString() const; PARTY_SCOPE: + void InitLocalUser(); void Initialize(const FUniqueNetIdRepl& PrimaryId); void NotifyPresenceChanged(ESocialSubsystem SubsystemType); diff --git a/Engine/Plugins/Online/OnlineFramework/Source/Qos/Private/QosStats.cpp b/Engine/Plugins/Online/OnlineFramework/Source/Qos/Private/QosStats.cpp index f1a435aa8d9a..328aaff5ea95 100644 --- a/Engine/Plugins/Online/OnlineFramework/Source/Qos/Private/QosStats.cpp +++ b/Engine/Plugins/Online/OnlineFramework/Source/Qos/Private/QosStats.cpp @@ -151,8 +151,8 @@ void FQosDatacenterStats::Upload(TSharedPtr& AnalyticsProvid * @EventParam NumResults integer Total number of results found for consideration * @EventParam NumSuccessCount integer Total number of successful ping evaluations * @EventParam NetworkType string type of network the client is connected to. (Unknown, None, AirplaneMode, Cell, Wifi, Ethernet) are possible values. Will be Unknown on PC and Switch. - * @EventParam BestRegionId string RegionId with best ping (that is usable) - * @EventParam BestRegionPing integer ping in the best RegionId (that is usable) + * @EventParam BestRegionId string RegionId with best ping (that is usable, UNREACHABLE if none pass QoS) + * @EventParam BestRegionPing integer ping in the best RegionId (that is usable, 0 if BestRegionId is UNREACHABLE) * @EventParam RegionDetails json representation of ping details * @Comments Analytics data for a complete qos datacenter determination attempt * @@ -190,6 +190,11 @@ void FQosDatacenterStats::ParseQosResults(TSharedPtr& Analyt QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionId, BestRegionId)); QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionPing, BestPing)); } + else + { + QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionId, TEXT("UNREACHABLE"))); + QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionPing, 0)); + } } { diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Private/OnlineAsyncTaskManager.cpp b/Engine/Plugins/Online/OnlineSubsystem/Source/Private/OnlineAsyncTaskManager.cpp index d16cc38f5fa9..4ba813fe99a3 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Private/OnlineAsyncTaskManager.cpp +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Private/OnlineAsyncTaskManager.cpp @@ -6,6 +6,7 @@ #include "HAL/Event.h" #include "Misc/ConfigCacheIni.h" #include "HAL/IConsoleManager.h" +#include "HAL/LowLevelMemTracker.h" #include "OnlineSubsystem.h" int32 FOnlineAsyncTaskManager::InvocationCount = 0; @@ -50,6 +51,8 @@ bool FOnlineAsyncTaskManager::Init(void) uint32 FOnlineAsyncTaskManager::Run(void) { + LLM_SCOPE(ELLMTag::Networking); + InvocationCount++; // This should not be set yet check(OnlineThreadId == 0); diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/IMessageSanitizerInterface.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/IMessageSanitizerInterface.h index 006926986642..c2434b46d700 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/IMessageSanitizerInterface.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/IMessageSanitizerInterface.h @@ -6,8 +6,16 @@ struct FBlockedQueryResult { + FBlockedQueryResult(bool InIsBlocked=false, bool InIsBlockedNonFriends=false, const FString& InUserId = FString()) + : bIsBlocked(InIsBlocked) + , bIsBlockedNonFriends(InIsBlockedNonFriends) + , UserId(InUserId) + {} + /** Is this user blocked */ bool bIsBlocked; + /** Is this user blocked for non-friends */ + bool bIsBlockedNonFriends; /** Platform specific unique id */ FString UserId; }; @@ -31,9 +39,10 @@ public: * * @param LocalUserNum local user making the query * @param FromUserId platform specific user id of the remote user + * @param FromPlatform platform for remote user * @param CompletionDelegate delegate to fire on completion */ - virtual void QueryBlockedUser(int32 LocalUserNum, const FString& FromUserId, const FOnQueryUserBlockedResponse& CompletionDelegate) = 0; + virtual void QueryBlockedUser(int32 LocalUserNum, const FString& FromUserId, const FString& FromPlatform, const FOnQueryUserBlockedResponse& CompletionDelegate) = 0; /** Invalidate all previously queried blocked users state */ virtual void ResetBlockedUserCache() = 0; diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlinePartyInterface.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlinePartyInterface.h index a030cb4ead69..aa21a7bf01a0 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlinePartyInterface.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlinePartyInterface.h @@ -607,6 +607,14 @@ PARTY_DECLARE_DELEGATETYPE(OnPartyExited); DECLARE_MULTICAST_DELEGATE_ThreeParams(F_PREFIX(OnPartyStateChanged), const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, EPartyState /*State*/); PARTY_DECLARE_DELEGATETYPE(OnPartyStateChanged); +/** +* Notification when a player has been approved for JIP +* @param LocalUserId - id associated with this notification +* @param PartyId - id associated with the party +*/ +DECLARE_MULTICAST_DELEGATE_ThreeParams(F_PREFIX(OnPartyJIP), const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, bool /*Success*/); +PARTY_DECLARE_DELEGATETYPE(OnPartyJIP); + /** * Notification when player promotion is locked out. * @param LocalUserId - id associated with this notification @@ -719,6 +727,17 @@ PARTY_DECLARE_DELEGATETYPE(OnPartyInviteResponseReceived); DECLARE_MULTICAST_DELEGATE_FiveParams(F_PREFIX(OnPartyJoinRequestReceived), const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, const FUniqueNetId& /*SenderId*/, const FString& /*Platform*/, const FOnlinePartyData& /*PartyData*/); PARTY_DECLARE_DELEGATETYPE(OnPartyJoinRequestReceived); +/** +* Notification when a new reservation request is received +* @param LocalUserId - id associated with this notification +* @param PartyId - id associated with the party +* @param SenderId - id of member that sent the request +* @param Platform - platform of member that sent the request +* @param PartyData - data provided by the sender for the leader to use to determine joinability +*/ +DECLARE_MULTICAST_DELEGATE_ThreeParams(F_PREFIX(OnPartyJIPRequestReceived), const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, const FUniqueNetId& /*SenderId*/); +PARTY_DECLARE_DELEGATETYPE(OnPartyJIPRequestReceived); + /** * Notification when a player wants to know if the party is in a joinable state * @param LocalUserId - id associated with this notification @@ -794,6 +813,17 @@ public: */ virtual bool JoinParty(const FUniqueNetId& LocalUserId, const IOnlinePartyJoinInfo& OnlinePartyJoinInfo, const FOnJoinPartyComplete& Delegate = FOnJoinPartyComplete()) = 0; + /** + * Join an existing game session from within a party + * + * @param LocalUserId - user making the request + * @param OnlinePartyJoinInfo - join information containing data such as party id, leader id + * @param Delegate - called on completion + * + * @return true if task was started + */ + virtual bool JIPFromWithinParty(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& PartyLeaderId) = 0; + /** * Query a party to check it's current joinability * Intended to be used before a call to LeaveParty (to leave your existing party, which would then be followed by JoinParty) @@ -843,6 +873,19 @@ public: */ virtual bool ApproveJoinRequest(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& RecipientId, bool bIsApproved, int32 DeniedResultCode = 0) = 0; + /** + * Approve a request to join the JIP match a party is in. + * + * @param LocalUserId - user making the request + * @param PartyId - id of an existing party + * @param RecipientId - id of the user being invited + * @param bIsApproved - whether the join request was approved or not + * @param DeniedResultCode - used when bIsApproved is false - client defined value to return when leader denies approval + * + * @return true if task was started + */ + virtual bool ApproveJIPRequest(const FUniqueNetId& LocalUserId, const FOnlinePartyId& PartyId, const FUniqueNetId& RecipientId, bool bIsApproved, int32 DeniedResultCode = 0) = 0; + /** * Respond to a query joinability request. This reflects the current party's joinability state and can change from moment to moment, and therefore does not guarantee a successful join. * @@ -1214,6 +1257,13 @@ public: */ DEFINE_ONLINE_DELEGATE_THREE_PARAM(OnPartyStateChanged, const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, EPartyState /*State*/); + /** + * notification of when a player had been approved to Join In Progress + * @param LocalUserId - id associated with this notification + * @param PartyId - id associated with the party + */ + DEFINE_ONLINE_DELEGATE_THREE_PARAM(OnPartyJIP, const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, bool /*Success*/); + /** * Notification when player promotion is locked out. * @@ -1315,6 +1365,17 @@ public: */ DEFINE_ONLINE_DELEGATE_FIVE_PARAM(OnPartyJoinRequestReceived, const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, const FUniqueNetId& /*SenderId*/, const FString& /*Platform*/, const FOnlinePartyData& /*PartyData*/); + /** + * Notification when a new reservation request is received + * Subscriber is expected to call ApproveJoinRequest + * @param LocalUserId - id associated with this notification + * @param PartyId - id associated with the party + * @param SenderId - id of member that sent the request + * @param Platform - platform of member that sent the request + * @param PartyData - data provided by the sender for the leader to use to determine joinability + */ + DEFINE_ONLINE_DELEGATE_THREE_PARAM(OnPartyJIPRequestReceived, const FUniqueNetId& /*LocalUserId*/, const FOnlinePartyId& /*PartyId*/, const FUniqueNetId& /*SenderId*/); + /** * Notification when a player wants to know if the party is in a joinable state * Subscriber is expected to call RespondToQueryJoinability diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h index 789aba263c60..970f67906b62 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h @@ -528,6 +528,23 @@ public: */ virtual bool FindSessionById(const FUniqueNetId& SearchingUserId, const FUniqueNetId& SessionId, const FUniqueNetId& FriendId, const FOnSingleSessionResultCompleteDelegate& CompletionDelegate) = 0; + /** + * Find a single advertised session by session id (with userdata) + * + * @param SearchingUserId user initiating the request + * @param Platform platform the session is on + * @param SessionId session id to search for + * @param FriendId optional id of user to verify in session + * @param UserData optional data that may be required by search + * @param CompletionDelegate delegate to call on completion + * + * @return true on success, false otherwise + */ + virtual bool FindSessionById(const FUniqueNetId& SearchingUserId, const FUniqueNetId& SessionId, const FUniqueNetId& FriendId, const FString& UserData, const FOnSingleSessionResultCompleteDelegate& CompletionDelegate) + { + return FindSessionById(SearchingUserId, SessionId, FriendId, CompletionDelegate); + } + /** * Cancels the current search in progress if possible for that search type * diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineUserCloudInterface.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineUserCloudInterface.h index c9a45c5f0389..08220a220cfc 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineUserCloudInterface.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineUserCloudInterface.h @@ -235,18 +235,18 @@ public: * Starts an asynchronous delete of the specified user file from the network platform's file store * * @param UserId User owning the storage - * @param FileToRead the name of the file to read + * @param FileName the name of the file to delete * @param bShouldCloudDelete whether to delete the file from the cloud * @param bShouldLocallyDelete whether to delete the file locally * - * @return true if the calls starts successfully, false otherwise + * @return true if the call starts successfully, false otherwise */ virtual bool DeleteUserFile(const FUniqueNetId& UserId, const FString& FileName, bool bShouldCloudDelete, bool bShouldLocallyDelete) = 0; /** * Delegate fired when a user file delete from the network platform's storage is complete * - * @param bWasSuccessful whether the file read was successful or not + * @param bWasSuccessful whether the file delete was successful or not * @param UserId User owning the storage * @param FileName the name of the file this was for */ diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineErrorMacros.inl b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineErrorMacros.inl index 3ee715a4011e..8bb5f6dc1cc0 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineErrorMacros.inl +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineErrorMacros.inl @@ -10,6 +10,9 @@ namespace Errors { + // Configured namspace + inline const TCHAR* BaseNamespace() { return TEXT(ONLINE_ERROR_NAMESPACE); } + // Configuration inline FOnlineError NotConfigured() { return ONLINE_ERROR(EOnlineErrorResult::NotConfigured); } diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineJsonSerializer.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineJsonSerializer.h index 70afaaf36800..8b37575d2593 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineJsonSerializer.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineJsonSerializer.h @@ -17,6 +17,7 @@ #define ONLINE_JSON_SERIALIZE_ARRAY_SERIALIZABLE(JsonName, JsonArray, ElementType) JSON_SERIALIZE_ARRAY_SERIALIZABLE(JsonName, JsonArray, ElementType) #define ONLINE_JSON_SERIALIZE_MAP_SERIALIZABLE(JsonName, JsonMap, ElementType) JSON_SERIALIZE_MAP_SERIALIZABLE(JsonName, JsonMap, ElementType) #define ONLINE_JSON_SERIALIZE_OBJECT_SERIALIZABLE(JsonName, JsonSerializableObject) JSON_SERIALIZE_OBJECT_SERIALIZABLE(JsonName, JsonSerializableObject) +#define ONLINE_JSON_SERIALIZE_DATETIME_UNIX_TIMESTAMP(JsonName, JsonDateTime) JSON_SERIALIZE_DATETIME_UNIX_TIMESTAMP(JsonName, JsonDateTime) typedef FJsonSerializable FOnlineJsonSerializable; typedef FJsonSerializerWriter<> FOnlineJsonSerializerWriter; diff --git a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h index e64b154b1f81..11c4192236d0 100644 --- a/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h +++ b/Engine/Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h @@ -31,6 +31,8 @@ #define SETTING_NEEDS FName(TEXT("NEEDS")) /** Second key for "needs" because can't set same value with two criteria (value is int32) */ #define SETTING_NEEDSSORT FName(TEXT("NEEDSSORT")) +/** Setting describing the session key (value is FString) */ +#define SETTING_SESSIONKEY FName(TEXT("SESSIONKEY")) /** 8 user defined integer params to be used when filtering searches for sessions */ #define SETTING_CUSTOMSEARCHINT1 FName(TEXT("CUSTOMSEARCHINT1")) diff --git a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook.Build.cs b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook.Build.cs index 1b85dbf96c4d..dd32112cddd2 100644 --- a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook.Build.cs +++ b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook.Build.cs @@ -53,7 +53,7 @@ public class OnlineSubsystemFacebook : ModuleRules System.Console.WriteLine(Err); PublicDefinitions.Add("WITH_FACEBOOK=1"); - PublicDefinitions.Add("UE4_FACEBOOK_VER=4.19.0"); + PublicDefinitions.Add("UE4_FACEBOOK_VER=4.39.0"); PrivateDependencyModuleNames.AddRange( new string[] { diff --git a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook_UPL.xml b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook_UPL.xml index 43d0c7760b17..def7778d263b 100644 --- a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook_UPL.xml +++ b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/OnlineSubsystemFacebook_UPL.xml @@ -16,6 +16,8 @@ + + @@ -59,7 +61,7 @@ dependencies { - implementation('com.facebook.android:facebook-android-sdk:4.32.0') + implementation('com.facebook.android:facebook-android-sdk:4.39.0') } @@ -74,6 +76,24 @@ dependencies { + + + + + + + + + + + + + + + + + + @@ -197,20 +217,24 @@ dependencies { - + + + + + - // Begin Facebook onCreate - facebookLogin = new FacebookLogin(this, Log); - if (!facebookLogin.init(BuildConfiguration)) - { - facebookLogin = null; - Log.error("Facebook SDK failed to initialize!"); - } - else - { - Log.debug("Facebook SDK success!"); - } - // End Facebook onCreate + // Begin Facebook onCreate + facebookLogin = new FacebookLogin(this, Log); + if (!facebookLogin.init(BuildConfiguration, bEnableAppEvents, bEnableAdId)) + { + facebookLogin = null; + Log.error("Facebook SDK failed to initialize!"); + } + else + { + Log.debug("Facebook SDK success!"); + } + // End Facebook onCreate diff --git a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/Private/IOS/OnlineSubsystemFacebook.cpp b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/Private/IOS/OnlineSubsystemFacebook.cpp index 8efbf46e79de..720bd81a9622 100644 --- a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/Private/IOS/OnlineSubsystemFacebook.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/Private/IOS/OnlineSubsystemFacebook.cpp @@ -21,16 +21,6 @@ FOnlineSubsystemFacebook::FOnlineSubsystemFacebook(FName InInstanceName) : FOnlineSubsystemFacebookCommon(InInstanceName) { - FString IOSFacebookAppID; - if (!GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("FacebookAppID"), IOSFacebookAppID, GEngineIni)) - { - UE_LOG_ONLINE(Warning, TEXT("The [IOSRuntimeSettings]:FacebookAppID has not been set")); - } - - if (ClientId.IsEmpty() || IOSFacebookAppID.IsEmpty() || (IOSFacebookAppID != ClientId)) - { - UE_LOG_ONLINE(Warning, TEXT("Inconsistency between OnlineSubsystemFacebook AppId [%s] and IOSRuntimeSettings AppId [%s]"), *ClientId, *IOSFacebookAppID); - } } FOnlineSubsystemFacebook::~FOnlineSubsystemFacebook() @@ -47,15 +37,18 @@ static void OnFacebookOpenURL(UIApplication* application, NSURL* url, NSString* static void OnFacebookAppDidBecomeActive() { +#if 0 // turn off analytics dispatch_async(dispatch_get_main_queue(), ^ { [FBSDKAppEvents activateApp]; }); +#endif } /** Add verbose logging for various Facebook SDK features */ void SetFBLoggingBehavior() { + [FBSDKSettings enableLoggingBehavior:FBSDKLoggingBehaviorAppEvents]; #if FACEBOOK_DEBUG_ENABLED [FBSDKSettings enableLoggingBehavior:FBSDKLoggingBehaviorAccessTokens]; [FBSDKSettings enableLoggingBehavior:FBSDKLoggingBehaviorPerformanceCharacteristics]; @@ -79,10 +72,10 @@ void PrintSDKStatus() NSString* OverrideAppId = [FBSDKAppEvents loggingOverrideAppID]; NSSet* LoggingBehaviors = [FBSDKSettings loggingBehavior]; - UE_LOG_ONLINE(Verbose, TEXT("Facebook SDK:%s"), *FString(SDKVersion)); - UE_LOG_ONLINE(Verbose, TEXT("AppId:%s"), *FString(AppId)); - UE_LOG_ONLINE(Verbose, TEXT("OverridAppId:%s"), *FString(OverrideAppId)); - UE_LOG_ONLINE(Verbose, TEXT("GraphVer:%s"), *FString(GraphVer)); + UE_LOG_ONLINE(Log, TEXT("Facebook SDK:%s"), *FString(SDKVersion)); + UE_LOG_ONLINE(Log, TEXT("AppId:%s"), *FString(AppId)); + UE_LOG_ONLINE(Log, TEXT("OverrideAppId:%s"), *FString(OverrideAppId)); + UE_LOG_ONLINE(Log, TEXT("GraphVer:%s"), *FString(GraphVer)); if (LoggingBehaviors != nil && [LoggingBehaviors count] > 0) { @@ -99,6 +92,17 @@ bool FOnlineSubsystemFacebook::Init() bool bSuccessfullyStartedUp = false; if (FOnlineSubsystemFacebookCommon::Init()) { + FString IOSFacebookAppID; + if (!GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("FacebookAppID"), IOSFacebookAppID, GEngineIni)) + { + UE_LOG_ONLINE(Warning, TEXT("The [IOSRuntimeSettings]:FacebookAppID has not been set")); + } + + if (ClientId.IsEmpty() || IOSFacebookAppID.IsEmpty() || (IOSFacebookAppID != ClientId)) + { + UE_LOG_ONLINE(Warning, TEXT("Inconsistency between OnlineSubsystemFacebook AppId [%s] and IOSRuntimeSettings AppId [%s]"), *ClientId, *IOSFacebookAppID); + } + FIOSCoreDelegates::OnOpenURL.AddStatic(&OnFacebookOpenURL); FCoreDelegates::ApplicationHasReactivatedDelegate.AddStatic(&OnFacebookAppDidBecomeActive); @@ -118,17 +122,49 @@ bool FOnlineSubsystemFacebook::Init() [FBSDKSettings setGraphAPIVersion:APIVerStr]; SetFBLoggingBehavior(); + /** Sets whether data such as that generated through FBSDKAppEvents and sent to Facebook should be restricted from being used for other than analytics and conversions */ + [FBSDKSettings setLimitEventAndDataUsage:TRUE]; + + bool bEnableAutomaticLogging = false; + if (GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bEnableAutomaticLogging"), bEnableAutomaticLogging, GEngineIni) && bEnableAutomaticLogging) + { + UE_LOG_ONLINE(Log, TEXT("AutologAppEvents: Enabled")); + [FBSDKSettings setAutoLogAppEventsEnabled:[NSNumber numberWithBool: YES]]; + } + else + { + UE_LOG_ONLINE(Log, TEXT("AutologAppEvents: Disabled")); + [FBSDKSettings setAutoLogAppEventsEnabled:[NSNumber numberWithBool: NO]]; + } + +#if 0 + bool bEnableAdvertisingId = false; + if (GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bEnableAdvertisingId"), bEnableAdvertisingId, GEngineIni) && bEnableAdvertisingId) + { + UE_LOG_ONLINE(Log, TEXT("AdvertiserId collection: Enabled")); + [FBSDKSettings setAdvertiserIDCollectionEnabled:1]; + } + else + { + UE_LOG_ONLINE(Log, TEXT("AdvertiserId collection: Disabled")); + [[FBSDKSettings setAdvertiserIDCollectionEnabled:0]; + } +#endif + // Trigger Facebook SDK last now that everything is setup dispatch_async(dispatch_get_main_queue(), ^ { UIApplication* sharedApp = [UIApplication sharedApplication]; NSDictionary* launchDict = [IOSAppDelegate GetDelegate].launchOptions; - if (!AnalyticsId.IsEmpty()) + if (bEnableAutomaticLogging) { - NSString* AnalyticsStr = [NSString stringWithFString:AnalyticsId]; - [FBSDKAppEvents setLoggingOverrideAppID:AnalyticsStr]; + if (!AnalyticsId.IsEmpty()) + { + NSString* AnalyticsStr = [NSString stringWithFString:AnalyticsId]; + [FBSDKAppEvents setLoggingOverrideAppID:AnalyticsStr]; + } + [FBSDKAppEvents activateApp]; } - [FBSDKAppEvents activateApp]; [[FBSDKApplicationDelegate sharedInstance] application:sharedApp didFinishLaunchingWithOptions: launchDict]; PrintSDKStatus(); }); diff --git a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/ThirdParty/Android/Java/FacebookLogin.java b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/ThirdParty/Android/Java/FacebookLogin.java index ba046d1bf87c..6fbf4cfe394a 100644 --- a/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/ThirdParty/Android/Java/FacebookLogin.java +++ b/Engine/Plugins/Online/OnlineSubsystemFacebook/Source/ThirdParty/Android/Java/FacebookLogin.java @@ -86,7 +86,7 @@ public class FacebookLogin * Hook into the various SDK features * https://developers.facebook.com/docs/reference/android/current/interface/FacebookCallback/ */ - public boolean init(String BuildConfiguration) + public boolean init(String BuildConfiguration, boolean bEnableAppEvents, boolean bEnableAdId) { boolean bShippingBuild = BuildConfiguration.equals("Shipping"); @@ -120,6 +120,9 @@ public class FacebookLogin FBLog.debug("Facebook SDK v" + SDKVersion); FBLog.debug(" API: " + APIVersion + " AppId: " + AppId + " Enabled: " + bIsFacebookEnabled + " Debug: " + bIsDebugEnabled); + FacebookSdk.setAutoLogAppEventsEnabled(bEnableAppEvents); + FacebookSdk.setAdvertiserIDCollectionEnabled(bEnableAdId); + callbackManager = CallbackManager.Factory.create(); LoginManager.getInstance().registerCallback(callbackManager, GetLoginCallback()); diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Classes/ConnectionCallbackProxy.h b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Classes/ConnectionCallbackProxy.h index 5ec8363bf517..4a514c59bc51 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Classes/ConnectionCallbackProxy.h +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Classes/ConnectionCallbackProxy.h @@ -29,7 +29,7 @@ class UConnectionCallbackProxy : public UOnlineBlueprintCallProxyBase // Connects to an online service such as Google Play UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage="Please use Show External Login UI instead", BlueprintInternalUseOnly = "true", WorldContext="WorldContextObject"), Category = "Online|Achievements") - static UConnectionCallbackProxy* ConnectToService(UObject* WorldContextObject, class APlayerController* PlayerController); + static UConnectionCallbackProxy* ConnectToService(UObject* WorldContextObject, class APlayerController* PlayerController); // UOnlineBlueprintCallProxyBase interface virtual void Activate() override; diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/IpNetDriver.cpp b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/IpNetDriver.cpp index 77ca309b9575..3059a8b4b903 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/IpNetDriver.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/IpNetDriver.cpp @@ -164,9 +164,28 @@ bool UIpNetDriver::InitBase( bool bInitAsClient, FNetworkNotify* InNotify, const Socket->SetSendBufferSize(SendSize,SendSize); UE_LOG(LogInit, Log, TEXT("%s: Socket queue %i / %i"), SocketSubsystem->GetSocketAPIName(), RecvSize, SendSize ); + // allow multihome as a url option + const TCHAR* MultiHomeBindAddr = URL.GetOption(TEXT("multihome="), nullptr); + if (MultiHomeBindAddr != nullptr) + { + LocalAddr = SocketSubsystem->CreateInternetAddr(); + + bool bIsValid = false; + LocalAddr->SetIp(MultiHomeBindAddr, bIsValid); + + if (!bIsValid) + { + LocalAddr = nullptr; + UE_LOG(LogNet, Warning, TEXT("Failed to created valid address from multihome address: %s"), MultiHomeBindAddr); + } + } + + if (!LocalAddr.IsValid()) + { + LocalAddr = SocketSubsystem->GetLocalBindAddr(*GLog); + } + // Bind socket to our port. - LocalAddr = SocketSubsystem->GetLocalBindAddr(*GLog); - LocalAddr->SetPort(bInitAsClient ? GetClientPort() : URL.Port); int32 AttemptPort = LocalAddr->GetPort(); @@ -613,6 +632,20 @@ UNetConnection* UIpNetDriver::ProcessConnectionlessPacket(const TSharedRefDescribe(), *IncomingAddress); diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlineBeaconClient.cpp b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlineBeaconClient.cpp index 58ed084587d9..9f33be8d67e5 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlineBeaconClient.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlineBeaconClient.cpp @@ -135,6 +135,8 @@ bool AOnlineBeaconClient::InitClient(FURL& URL) } #endif + SetConnectionState(EBeaconConnectionState::Pending); + // Kick off the connection handshake bool bSentHandshake = false; @@ -146,20 +148,32 @@ bool AOnlineBeaconClient::InitClient(FURL& URL) bSentHandshake = true; } - SetConnectionState(EBeaconConnectionState::Pending); - - NetDriver->SetWorld(World); - NetDriver->Notify = this; - NetDriver->InitialConnectTimeout = BeaconConnectionInitialTimeout; - NetDriver->ConnectionTimeout = BeaconConnectionTimeout; - - - if (!bSentHandshake) + if (NetDriver) { - SendInitialJoin(); - } + NetDriver->SetWorld(World); + NetDriver->Notify = this; + NetDriver->InitialConnectTimeout = BeaconConnectionInitialTimeout; + NetDriver->ConnectionTimeout = BeaconConnectionTimeout; - bSuccess = true; + if (!bSentHandshake) + { + SendInitialJoin(); + } + + bSuccess = true; + } + else + { + // an error must have occurred during BeginHandshaking + UE_LOG(LogBeacon, Warning, TEXT("AOnlineBeaconClient::InitClient BeginHandshaking failed")); + + // if the connection is still pending, notify of failure + if (GetConnectionState() == EBeaconConnectionState::Pending) + { + SetConnectionState(EBeaconConnectionState::Invalid); + OnFailure(); + } + } } else { diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlinePIESettings.cpp b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlinePIESettings.cpp index 95f657a93b9d..bcf6dd3849dc 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlinePIESettings.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/OnlinePIESettings.cpp @@ -119,10 +119,21 @@ void UOnlinePIESettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyC FName SubPropName = PropertyChangedEvent.Property->GetFName(); if (SubPropName == GET_MEMBER_NAME_CHECKED(FPIELoginSettingsInternal, Id)) { + TSet Ids; for (FPIELoginSettingsInternal& Login : Logins) { // Remove any whitespace from login input Login.Id.TrimStartAndEndInline(); + + if (Ids.Contains(Login.Id)) + { + // Don't allow duplicate login ids + Login.Id.Reset(); + } + else + { + Ids.Add(Login.Id); + } } } else if (SubPropName == GET_MEMBER_NAME_CHECKED(FPIELoginSettingsInternal, Token)) diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/PartyBeaconState.cpp b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/PartyBeaconState.cpp index 24faa806e67d..4675ea3cfcc1 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/PartyBeaconState.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/PartyBeaconState.cpp @@ -584,6 +584,8 @@ bool UPartyBeaconState::DoesReservationFit(const FPartyReservation& ReservationR const bool bPartySizeOk = (IncomingPartySize > 0) && (IncomingPartySize <= NumPlayersPerTeam); const bool bRoomForReservation = (NumConsumedReservations + IncomingPartySize ) <= MaxReservations; + UE_LOG(LogPartyBeacon, Verbose, TEXT("UPartyBeaconState::DoesReservationFit: Incoming Party Size: %d Num Players Per Team: %d NumConsumedReservations: %d MaxReservations: %d"), IncomingPartySize, NumPlayersPerTeam, NumConsumedReservations, MaxReservations); + return bPartySizeOk && bRoomForReservation; } @@ -956,6 +958,29 @@ int32 UPartyBeaconState::GetExistingReservationContainingMember(const FUniqueNet return Result; } +bool UPartyBeaconState::UpdateMemberPlatform(const FUniqueNetIdRepl& PartyMember, const FString& PlatformName) +{ + for (int32 ResIdx = 0; ResIdx < Reservations.Num(); ResIdx++) + { + FPartyReservation& ReservationEntry = Reservations[ResIdx]; + for (FPlayerReservation& PlayerReservation : ReservationEntry.PartyMembers) + { + if (PlayerReservation.UniqueId == PartyMember) + { + if (!PlatformName.IsEmpty()) + { + PlayerReservation.Platform = PlatformName; + } + // Return that member was updated + return true; + } + } + } + + //Return that member was not updated + return false; +} + bool UPartyBeaconState::PlayerHasReservation(const FUniqueNetId& PlayerId) const { bool bFound = false; diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/VoiceEngineImpl.cpp b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/VoiceEngineImpl.cpp index 3e115900edd3..fa02f9a2c031 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/VoiceEngineImpl.cpp +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Private/VoiceEngineImpl.cpp @@ -8,6 +8,7 @@ #include "Sound/SoundWaveProcedural.h" #include "OnlineSubsystemUtils.h" #include "GameFramework/GameSession.h" +#include "OnlineSubsystemBPCallHelper.h" /** Largest size allowed to carry over into next buffer */ #define MAX_VOICE_REMAINDER_SIZE 4 * 1024 @@ -35,6 +36,22 @@ FRemoteTalkerDataImpl::FRemoteTalkerDataImpl() : } } +FRemoteTalkerDataImpl::FRemoteTalkerDataImpl(const FRemoteTalkerDataImpl& Other) +{ + LastSeen = Other.LastSeen; + NumFramesStarved = Other.NumFramesStarved; + VoipSynthComponent = Other.VoipSynthComponent; + VoiceDecoder = Other.VoiceDecoder; + MaxUncompressedDataSize = Other.MaxUncompressedDataSize; + MaxUncompressedDataQueueSize = Other.MaxUncompressedDataQueueSize; + CurrentUncompressedDataQueueSize = Other.CurrentUncompressedDataQueueSize; + + { + FScopeLock ScopeLock(&Other.QueueLock); + UncompressedDataQueue = Other.UncompressedDataQueue; + } +} + FRemoteTalkerDataImpl::FRemoteTalkerDataImpl(FRemoteTalkerDataImpl&& Other) { LastSeen = Other.LastSeen; @@ -107,6 +124,20 @@ void FRemoteTalkerDataImpl::Cleanup() VoipSynthComponent = nullptr; } +FVoiceEngineImpl ::FVoiceEngineImpl() : + OnlineSubsystem(nullptr), + VoiceCapture(nullptr), + VoiceEncoder(nullptr), + OwningUserIndex(INVALID_INDEX), + UncompressedBytesAvailable(0), + CompressedBytesAvailable(0), + AvailableVoiceResult(EVoiceCaptureState::UnInitialized), + bPendingFinalCapture(false), + bIsCapturing(false), + SerializeHelper(nullptr) +{ +} + FVoiceEngineImpl::FVoiceEngineImpl(IOnlineSubsystem* InSubsystem) : OnlineSubsystem(InSubsystem), VoiceCapture(nullptr), @@ -139,7 +170,7 @@ FVoiceEngineImpl::~FVoiceEngineImpl() void FVoiceEngineImpl::VoiceCaptureUpdate() const { - if (bPendingFinalCapture) + if (bPendingFinalCapture && VoiceCapture.IsValid()) { uint32 CompressedSize; const EVoiceCaptureState::Type RecordingState = VoiceCapture->GetCaptureState(CompressedSize); @@ -284,6 +315,28 @@ uint32 FVoiceEngineImpl::StopLocalVoiceProcessing(uint32 LocalUserNum) return Return; } +uint32 FVoiceEngineImpl::RegisterLocalTalker(uint32 LocalUserNum) +{ + if (OwningUserIndex == INVALID_INDEX) + { + OwningUserIndex = LocalUserNum; + return ONLINE_SUCCESS; + } + + return ONLINE_FAIL; +} + +uint32 FVoiceEngineImpl::UnregisterLocalTalker(uint32 LocalUserNum) +{ + if (IsOwningUser(LocalUserNum)) + { + OwningUserIndex = INVALID_INDEX; + return ONLINE_SUCCESS; + } + + return ONLINE_FAIL; +} + uint32 FVoiceEngineImpl::UnregisterRemoteTalker(const FUniqueNetId& UniqueId) { FRemoteTalkerDataImpl* RemoteData = RemoteTalkerBuffers.Find(FUniqueNetIdWrapper(UniqueId.AsShared())); @@ -479,6 +532,8 @@ uint32 FVoiceEngineImpl::SubmitRemoteVoiceData(const FUniqueNetIdWrapper& Remote OwningTalker = UVOIPStatics::GetVOIPTalkerForPlayer(RemoteTalkerId, InSettings); + GetVoiceSettingsOverride(RemoteTalkerId, InSettings); + ApplyVoiceSettings(QueuedData.VoipSynthComponent, InSettings); QueuedData.VoipSynthComponent->ResetBuffer(InSampleCount, UVOIPStatics::GetBufferingDelay()); @@ -542,7 +597,10 @@ void FVoiceEngineImpl::TickTalkers(float DeltaTime) void FVoiceEngineImpl::Tick(float DeltaTime) { // Check available voice once a frame, this value changes after calling GetVoiceData() - AvailableVoiceResult = VoiceCapture->GetCaptureState(UncompressedBytesAvailable); + if (VoiceCapture.IsValid()) + { + AvailableVoiceResult = VoiceCapture->GetCaptureState(UncompressedBytesAvailable); + } TickTalkers(DeltaTime); } @@ -699,4 +757,9 @@ bool FVoiceEngineImpl::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar return bWasHandled; } +int32 FVoiceEngineImpl::GetMaxVoiceRemainderSize() +{ + return MAX_VOICE_REMAINDER_SIZE; +} + diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/PartyBeaconState.h b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/PartyBeaconState.h index 1c92e05d67c4..47fb18d5b97f 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/PartyBeaconState.h +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/PartyBeaconState.h @@ -472,6 +472,17 @@ class ONLINESUBSYSTEMUTILS_API UPartyBeaconState : public UObject */ virtual bool PlayerHasReservation(const FUniqueNetId& PlayerId) const; + /** + * Updates the platform on an existing reservation + * (Used when MMS does not set a platform when handing out a match assignment) + * + * @param PlayerId uniqueid of the player to update + * @param PlatformName platform to set new player as using + * + * @return true if a reservation exists, false otherwise + */ + virtual bool UpdateMemberPlatform(const FUniqueNetIdRepl& PartyMember, const FString& PlatformName); + /** * Obtain player validation string from party reservation entry * diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoiceEngineImpl.h b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoiceEngineImpl.h index db281a99a737..0cf702dcc212 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoiceEngineImpl.h +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoiceEngineImpl.h @@ -10,7 +10,6 @@ #include "Net/VoiceDataCommon.h" #include "Interfaces/VoiceCapture.h" #include "Interfaces/VoiceCodec.h" -#include "OnlineSubsystemBPCallHelper.h" #include "OnlineSubsystemUtilsPackage.h" #include "VoipListenerSynthComponent.h" #include "OnlineSubsystemUtilsPackage.h" @@ -42,12 +41,13 @@ struct FLocalVoiceData /** * Remote voice data playing on a single client */ -class FRemoteTalkerDataImpl +class ONLINESUBSYSTEMUTILS_API FRemoteTalkerDataImpl { public: FRemoteTalkerDataImpl(); /** Required for TMap FindOrAdd() */ + FRemoteTalkerDataImpl(const FRemoteTalkerDataImpl& Other); FRemoteTalkerDataImpl(FRemoteTalkerDataImpl&& Other); ~FRemoteTalkerDataImpl(); @@ -85,7 +85,7 @@ public: /** * Generic implementation of voice engine, using Voice module for capture/codec */ -class FVoiceEngineImpl : public IVoiceEngine, public FSelfRegisteringExec +class ONLINESUBSYSTEMUTILS_API FVoiceEngineImpl : public IVoiceEngine, public FSelfRegisteringExec { class FVoiceSerializeHelper : public FGCObject { @@ -195,18 +195,7 @@ class FVoiceEngineImpl : public IVoiceEngine, public FSelfRegisteringExec PACKAGE_SCOPE: /** Constructor */ - FVoiceEngineImpl() : - OnlineSubsystem(nullptr), - VoiceCapture(nullptr), - VoiceEncoder(nullptr), - OwningUserIndex(INVALID_INDEX), - UncompressedBytesAvailable(0), - CompressedBytesAvailable(0), - AvailableVoiceResult(EVoiceCaptureState::UnInitialized), - bPendingFinalCapture(false), - bIsCapturing(false), - SerializeHelper(nullptr) - {}; + FVoiceEngineImpl(); // IVoiceEngine virtual bool Init(int32 MaxLocalTalkers, int32 MaxRemoteTalkers) override; @@ -232,27 +221,8 @@ public: return ONLINE_SUCCESS; } - virtual uint32 RegisterLocalTalker(uint32 LocalUserNum) override - { - if (OwningUserIndex == INVALID_INDEX) - { - OwningUserIndex = LocalUserNum; - return ONLINE_SUCCESS; - } - - return ONLINE_FAIL; - } - - virtual uint32 UnregisterLocalTalker(uint32 LocalUserNum) override - { - if (IsOwningUser(LocalUserNum)) - { - OwningUserIndex = INVALID_INDEX; - return ONLINE_SUCCESS; - } - - return ONLINE_FAIL; - } + virtual uint32 RegisterLocalTalker(uint32 LocalUserNum) override; + virtual uint32 UnregisterLocalTalker(uint32 LocalUserNum) override; virtual uint32 RegisterRemoteTalker(const FUniqueNetId& UniqueId) override { @@ -300,6 +270,8 @@ public: // FSelfRegisteringExec virtual bool Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override; + virtual void GetVoiceSettingsOverride(const FUniqueNetIdWrapper& RemoteTalkerId, FVoiceSettings& VoiceSettings) {} + private: /** @@ -316,6 +288,16 @@ private: * Delegate that fixes up remote audio components when the level changes */ void OnPostLoadMap(UWorld*); + +protected: + IOnlineSubsystem* GetOnlineSubSystem() { return OnlineSubsystem; } + TSharedPtr& GetVoiceCapture() { return VoiceCapture; } + TSharedPtr& GetVoiceEncoder() { return VoiceEncoder; } + FRemoteTalkerData& GetRemoteTalkerBuffers() { return RemoteTalkerBuffers; } + TArray& GetCompressedVoiceBuffer() { return CompressedVoiceBuffer; } + TArray& GetDecompressedVoiceBuffer() { return DecompressedVoiceBuffer; } + FLocalVoiceData* GetLocalPlayerVoiceData() { return PlayerVoiceData; } + int32 GetMaxVoiceRemainderSize(); }; typedef TSharedPtr FVoiceEngineImplPtr; diff --git a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoicePacketImpl.h b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoicePacketImpl.h index 81ab403f8348..ff3ced437ede 100644 --- a/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoicePacketImpl.h +++ b/Engine/Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/VoicePacketImpl.h @@ -11,7 +11,7 @@ #define DEBUG_VOICE_PACKET_ENCODING 0 /** Defines the data involved in a voice packet */ -class FVoicePacketImpl : public FVoicePacket +class ONLINESUBSYSTEMUTILS_API FVoicePacketImpl : public FVoicePacket { PACKAGE_SCOPE: diff --git a/Engine/Plugins/Runtime/AndroidMoviePlayer/Source/AndroidMoviePlayer/Private/AndroidMovieStreamer.cpp b/Engine/Plugins/Runtime/AndroidMoviePlayer/Source/AndroidMoviePlayer/Private/AndroidMovieStreamer.cpp index 4bfe1389cd92..4d4e4f3af740 100644 --- a/Engine/Plugins/Runtime/AndroidMoviePlayer/Source/AndroidMoviePlayer/Private/AndroidMovieStreamer.cpp +++ b/Engine/Plugins/Runtime/AndroidMoviePlayer/Source/AndroidMoviePlayer/Private/AndroidMovieStreamer.cpp @@ -15,6 +15,10 @@ #include "Misc/ScopeLock.h" #include "Misc/Paths.h" +#include "HAL/FileManager.h" +#include "HAL/PlatformFilemanager.h" +#include "IPlatformFilePak.h" + DEFINE_LOG_CATEGORY_STATIC(LogAndroidMediaPlayerStreamer, Log, All); #define MOVIE_FILE_EXTENSION "mp4" @@ -187,30 +191,77 @@ bool FAndroidMediaPlayerStreamer::StartNextMovie() MovieQueue.RemoveAt(0); } - // Don't bother trying to play it if we can't find it. - if (!IAndroidPlatformFile::GetPlatformPhysical().FileExists(*MoviePath)) - { - return false; - } - bool movieOK = true; - // Get information about the movie. - int64 FileOffset = IAndroidPlatformFile::GetPlatformPhysical().FileStartOffset(*MoviePath); - int64 FileSize = IAndroidPlatformFile::GetPlatformPhysical().FileSize(*MoviePath); - FString FileRootPath = IAndroidPlatformFile::GetPlatformPhysical().FileRootPath(*MoviePath); + IAndroidPlatformFile& PlatformFile = IAndroidPlatformFile::GetPlatformPhysical(); - // Play the movie as a file or asset. - if (IAndroidPlatformFile::GetPlatformPhysical().IsAsset(*MoviePath)) + // Don't bother trying to play it if we can't find it. + if (!PlatformFile.FileExists(*MoviePath)) { - movieOK = JavaMediaPlayer->SetDataSource( - IAndroidPlatformFile::GetPlatformPhysical().GetAssetManager(), - FileRootPath, FileOffset, FileSize); + // possible it is in a PAK file + FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName())); + + FPakFile* PakFile = NULL; + FPakEntry FileEntry; + if (!PakPlatformFile->FindFileInPakFiles(*MoviePath, &PakFile, &FileEntry)) + { + return false; + } + + // is it a simple case (can just use file datasource)? + if (FileEntry.CompressionMethod == COMPRESS_None && !FileEntry.IsEncrypted()) + { + FString PakFilename = PakFile->GetFilename(); + int64 PakHeaderSize = FileEntry.GetSerializedSize(PakFile->GetInfo().Version); + int64 OffsetInPak = FileEntry.Offset + PakHeaderSize; + int64 FileSize = FileEntry.Size; + + int64 FileOffset = PlatformFile.FileStartOffset(*PakFilename) + OffsetInPak; + FString FileRootPath = PlatformFile.FileRootPath(*PakFilename); + + if (!JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize)) + { + return false; + } + } + else + { + // only support media data source on Android 6.0+ + if (FAndroidMisc::GetAndroidBuildVersion() < 23) + { + //UE_LOG(LogAndroidMedia, Warning, TEXT("File cannot be played on this version of Android due to PAK file with compression or encryption: %s."), *MoviePath); + return false; + } + + TSharedRef Archive = MakeShareable(IFileManager::Get().CreateFileReader(*MoviePath)); + + if (!JavaMediaPlayer->SetDataSource(Archive)) + { + //UE_LOG(LogAndroidMedia, Warning, TEXT("Failed to set data source for archive %s"), *MoviePath); + return false; + } + } } else { - movieOK = JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize); + // Get information about the movie. + int64 FileOffset = PlatformFile.FileStartOffset(*MoviePath); + int64 FileSize = PlatformFile.FileSize(*MoviePath); + FString FileRootPath = PlatformFile.FileRootPath(*MoviePath); + + // Play the movie as a file or asset. + if (PlatformFile.IsAsset(*MoviePath)) + { + movieOK = JavaMediaPlayer->SetDataSource( + IAndroidPlatformFile::GetPlatformPhysical().GetAssetManager(), + FileRootPath, FileOffset, FileSize); + } + else + { + movieOK = JavaMediaPlayer->SetDataSource(FileRootPath, FileOffset, FileSize); + } } + FIntPoint VideoDimensions; if (movieOK) { diff --git a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.cpp b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.cpp index ed2c4862cd68..2072865c2e6a 100644 --- a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.cpp +++ b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.cpp @@ -22,6 +22,8 @@ DECLARE_FLOAT_COUNTER_STAT(TEXT("Initial Tick (ms)"), STAT_AnimationBudgetAlloca DECLARE_DWORD_COUNTER_STAT(TEXT("Always Tick"), STAT_AnimationBudgetAllocator_AlwaysTick, STATGROUP_AnimationBudgetAllocator); DECLARE_DWORD_COUNTER_STAT(TEXT("Throttled"), STAT_AnimationBudgetAllocator_Throttled, STATGROUP_AnimationBudgetAllocator); DECLARE_DWORD_COUNTER_STAT(TEXT("Interpolated"), STAT_AnimationBudgetAllocator_Interpolated, STATGROUP_AnimationBudgetAllocator); +DECLARE_FLOAT_COUNTER_STAT(TEXT("SmoothedBudgetPressure"), STAT_AnimationBudgetAllocator_SmoothedBudgetPressure, STATGROUP_AnimationBudgetAllocator); + CSV_DEFINE_CATEGORY(AnimationBudget, true); @@ -197,6 +199,58 @@ static FAutoConsoleVariableRef CVarSkelBatch_StateChangeThrottleInFrames( }), ECVF_Scalability); +static float GBudgetFactorBeforeReducedWork = 1.5f; + +static FAutoConsoleVariableRef CVarSkelBatch_BudgetFactorBeforeReducedWork( + TEXT("a.Budget.BudgetFactorBeforeReducedWork"), + GBudgetFactorBeforeReducedWork, + TEXT("Range > 1, Default = 1.5\n") + TEXT("Reduced work will be delayed until budget pressure goes over this amount.\n"), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* InVariable) + { + GBudgetFactorBeforeReducedWork = FMath::Max(GBudgetFactorBeforeReducedWork, 1.0f); + }), + ECVF_Scalability); + +static float GBudgetFactorBeforeReducedWorkEpsilon = 0.25f; + +static FAutoConsoleVariableRef CVarSkelBatch_BudgetFactorBeforeReducedWorkEpsilon( + TEXT("a.Budget.BudgetFactorBeforeReducedWorkEpsilon"), + GBudgetFactorBeforeReducedWorkEpsilon, + TEXT("Range > 0.0, Default = 0.25\n") + TEXT("Increased work will be delayed until budget pressure goes under a.Budget.BudgetFactorBeforeReducedWork minus this amount.\n"), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* InVariable) + { + GBudgetFactorBeforeReducedWorkEpsilon = FMath::Max(GBudgetFactorBeforeReducedWorkEpsilon, 0.0f); + }), + ECVF_Scalability); + +static float GBudgetPressureSmoothingSpeed = 1.5f; + +static FAutoConsoleVariableRef CVarSkelBatch_BudgetPressureSmoothingSpeed( + TEXT("a.Budget.BudgetPressureSmoothingSpeed"), + GBudgetPressureSmoothingSpeed, + TEXT("Range > 0.0, Default = 1.5\n") + TEXT("How much to smooth the budget pressure value used to throttle reduced work.\n"), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* InVariable) + { + GBudgetPressureSmoothingSpeed = FMath::Max(GBudgetPressureSmoothingSpeed, KINDA_SMALL_NUMBER); + }), + ECVF_Scalability); + +static int32 GReducedWorkThrottleInFrames = 20; + +static FAutoConsoleVariableRef CVarSkelBatch_ReducedWorkThrottleInFrames( + TEXT("a.Budget.ReducedWorkThrottleInFrames"), + GStateChangeThrottleInFrames, + TEXT("Range [1, 255], Default = 20\n") + TEXT("Prevents reduced work from changing too often due to system and load noise.\n"), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* InVariable) + { + GReducedWorkThrottleInFrames = FMath::Clamp(GReducedWorkThrottleInFrames, 1, 255); + }), + ECVF_Scalability); + FComponentData::FComponentData(USkeletalMeshComponentBudgeted* InComponent) : Component(InComponent) , RootPrerequisite(nullptr) @@ -226,6 +280,8 @@ FAnimationBudgetAllocator::FAnimationBudgetAllocator(UWorld* InWorld) , NumComponentsToNotSkip(0) , TotalEstimatedTickTimeMs(0.0f) , NumWorkUnitsForAverage(0.0f) + , SmoothedBudgetPressure(0.0f) + , ReducedComponentWorkCounter(0) , CurrentFrameOffset(0) , bEnabled(false) { @@ -265,6 +321,26 @@ void FAnimationBudgetAllocator::SetComponentTickEnabled(USkeletalMeshComponentBu } } +bool FAnimationBudgetAllocator::IsComponentTickEnabled(USkeletalMeshComponentBudgeted* Component) const +{ +#if USE_SKEL_BATCHING + if (FAnimationBudgetAllocator::bCachedEnabled) + { + int32 Handle = Component->GetAnimationBudgetHandle(); + if(Handle != INDEX_NONE) + { + return AllComponentData[Handle].bTickEnabled; + } + + return Component->PrimaryComponentTick.IsTickFunctionEnabled(); + } + else +#endif + { + return Component->PrimaryComponentTick.IsTickFunctionEnabled(); + } +} + void FAnimationBudgetAllocator::SetComponentSignificance(USkeletalMeshComponentBudgeted* Component, float Significance, bool bAlwaysTick, bool bTickEvenIfNotRendered, bool bAllowReducedWork, bool bNeverThrottle) { #if USE_SKEL_BATCHING @@ -487,7 +563,7 @@ int32 FAnimationBudgetAllocator::CalculateWorkDistributionAndQueue(float InDelta // Reset completion time as it may not always be run InComponentData.GameThreadLastCompletionTimeMs = 0.0f; - InComponentData.Component->EnableExternalInterpolation(InComponentData.bInterpolate); + InComponentData.Component->EnableExternalInterpolation(InComponentData.TickRate > 1 && InComponentData.bInterpolate); InComponentData.Component->EnableExternalUpdate(bTickThisFrame); InComponentData.Component->EnableExternalEvaluationRateLimiting(InComponentData.TickRate > 1); InComponentData.Component->SetExternalDeltaTime(InComponentData.AccumulatedDeltaTime); @@ -528,7 +604,7 @@ int32 FAnimationBudgetAllocator::CalculateWorkDistributionAndQueue(float InDelta AverageWorkUnitTimeMs = FMath::FInterpTo(AverageWorkUnitTimeMs, AverageTickTimeMs, InDeltaSeconds, GWorkUnitSmoothingSpeed); SET_FLOAT_STAT(STAT_AnimationBudgetAllocator_AverageWorkUnitTime, AverageWorkUnitTimeMs); - BUDGET_CSV_STAT(AnimationBudget, AverageWorkUnitTimeMs, AverageWorkUnitTimeMs, ECsvCustomStatOp::Set); + CSV_CUSTOM_STAT(AnimationBudget, AverageWorkUnitTimeMs, AverageTickTimeMs, ECsvCustomStatOp::Set); // Want to map the remaining (non-fixed) work units so that we only execute N work units per frame. // If we can go over budget to keep quality then we use that value @@ -654,35 +730,49 @@ int32 FAnimationBudgetAllocator::CalculateWorkDistributionAndQueue(float InDelta } } - // If we have any components running reduced work when we have an excess, then move them out of the 'reduced' pool per tick - // + 2 to ensure we get some ceiling and avoid flip-flopping - if (ReducedWorkComponentData.Num() > 0 && TotalIdealWorkUnits + 2 < FMath::CeilToInt(WorkUnitBudget)) + const float BudgetPressure = (float)TotalIdealWorkUnits / WorkUnitBudget; + SmoothedBudgetPressure = FMath::FInterpTo(SmoothedBudgetPressure, BudgetPressure, InDeltaSeconds, GBudgetPressureSmoothingSpeed); + + SET_FLOAT_STAT(STAT_AnimationBudgetAllocator_SmoothedBudgetPressure, SmoothedBudgetPressure); + + if(--ReducedComponentWorkCounter <= 0) { - FComponentData& ComponentData = AllComponentData[ReducedWorkComponentData[0]]; - if(ComponentData.bReducedWork && ComponentData.Component->OnReduceWork().IsBound()) + // If we have any components running reduced work when we have an excess, then move them out of the 'reduced' pool per tick + if (ReducedWorkComponentData.Num() > 0 && SmoothedBudgetPressure < GBudgetFactorBeforeReducedWork - GBudgetFactorBeforeReducedWorkEpsilon) { -#if WITH_TICK_DEBUG - UE_LOG(LogTemp, Warning, TEXT("Increasing component work (mesh %s) (actor %llx)"), ComponentData.Component->SkeletalMesh ? *ComponentData.Component->SkeletalMesh->GetName() : TEXT("null"), (uint64)ComponentData.Component->GetOwner()); -#endif - ComponentData.Component->OnReduceWork().Execute(ComponentData.Component, false); - ComponentData.bReducedWork = false; - } - } - else - { - // Any work units that we interpolate or throttle should also be eligible for work reduction (which can involve disabling other ticks), so set them all now if needed - for (SortedComponentIndex = FullIndexEnd; SortedComponentIndex < TotalIdealWorkUnits; ++SortedComponentIndex) - { - FComponentData& ComponentData = AllComponentData[AllSortedComponentData[SortedComponentIndex]]; - if(ComponentData.bAllowReducedWork && !ComponentData.bReducedWork && ComponentData.Component->OnReduceWork().IsBound()) + for(int32 ReducedWorkComponentIndex : ReducedWorkComponentData) { + FComponentData& ComponentData = AllComponentData[ReducedWorkComponentIndex]; + if(ComponentData.bReducedWork && ComponentData.Component->OnReduceWork().IsBound()) + { #if WITH_TICK_DEBUG - UE_LOG(LogTemp, Warning, TEXT("Reducing component work (mesh %s) (actor %llx)"), ComponentData.Component->SkeletalMesh ? *ComponentData.Component->SkeletalMesh->GetName() : TEXT("null"), (uint64)ComponentData.Component->GetOwner()); + UE_LOG(LogTemp, Warning, TEXT("Increasing component work (mesh %s) (actor %llx)"), ComponentData.Component->SkeletalMesh ? *ComponentData.Component->SkeletalMesh->GetName() : TEXT("null"), (uint64)ComponentData.Component->GetOwner()); #endif - ComponentData.Component->OnReduceWork().Execute(ComponentData.Component, true); - ComponentData.bReducedWork = true; + ComponentData.Component->OnReduceWork().Execute(ComponentData.Component, false); + ComponentData.bReducedWork = false; + break; + } } } + else if(SmoothedBudgetPressure > GBudgetFactorBeforeReducedWork) + { + // Any work units that we interpolate or throttle should also be eligible for work reduction (which can involve disabling other ticks), so set them all now if needed + for (SortedComponentIndex = TotalIdealWorkUnits - 1; SortedComponentIndex >= FullIndexEnd; --SortedComponentIndex) + { + FComponentData& ComponentData = AllComponentData[AllSortedComponentData[SortedComponentIndex]]; + if(ComponentData.bAllowReducedWork && !ComponentData.bReducedWork && ComponentData.Component->OnReduceWork().IsBound()) + { +#if WITH_TICK_DEBUG + UE_LOG(LogTemp, Warning, TEXT("Reducing component work (mesh %s) (actor %llx)"), ComponentData.Component->SkeletalMesh ? *ComponentData.Component->SkeletalMesh->GetName() : TEXT("null"), (uint64)ComponentData.Component->GetOwner()); +#endif + ComponentData.Component->OnReduceWork().Execute(ComponentData.Component, true); + ComponentData.bReducedWork = true; + break; + } + } + } + + ReducedComponentWorkCounter = GReducedWorkThrottleInFrames; } } @@ -739,8 +829,8 @@ void FAnimationBudgetAllocator::Update(float DeltaSeconds) SET_DWORD_STAT(STAT_AnimationBudgetAllocator_NumTickedComponents, NumTicked); SET_DWORD_STAT(STAT_AnimationBudgetAllocator_NumRegisteredComponents, AllComponentData.Num()); BUDGET_CSV_STAT(AnimationBudget, NumTicked, NumTicked, ECsvCustomStatOp::Set); - CSV_CUSTOM_STAT(AnimationBudget, AnimQuality, AllSortedComponentData.Num() > 0 ? (float)NumTicked / (float)AllSortedComponentData.Num() : 0.0f, ECsvCustomStatOp::Set); - CSV_CUSTOM_STAT(AnimationBudget, AverageTickRate, AverageTickRate, ECsvCustomStatOp::Set); + BUDGET_CSV_STAT(AnimationBudget, AnimQuality, AllSortedComponentData.Num() > 0 ? (float)NumTicked / (float)AllSortedComponentData.Num() : 0.0f, ECsvCustomStatOp::Set); + BUDGET_CSV_STAT(AnimationBudget, AverageTickRate, AverageTickRate, ECsvCustomStatOp::Set); #if WITH_TICK_DEBUG for (int32 ComponentDataIndex : AllSortedComponentData) diff --git a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.h b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.h index 492234422754..8b63066fb59e 100644 --- a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.h +++ b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/AnimationBudgetAllocator.h @@ -136,6 +136,7 @@ public: virtual void UpdateComponentTickPrerequsites(USkeletalMeshComponentBudgeted* InComponent) override; virtual void SetComponentSignificance(USkeletalMeshComponentBudgeted* Component, float Significance, bool bNeverSkip = false, bool bTickEvenIfNotRendered = false, bool bAllowReducedWork = true, bool bForceInterpolate = false) override; virtual void SetComponentTickEnabled(USkeletalMeshComponentBudgeted* Component, bool bShouldTick) override; + virtual bool IsComponentTickEnabled(USkeletalMeshComponentBudgeted* Component) const override; virtual void SetGameThreadLastTickTimeMs(int32 InManagerHandle, float InGameThreadLastTickTimeMs) override; virtual void SetGameThreadLastCompletionTimeMs(int32 InManagerHandle, float InGameThreadLastCompletionTimeMs) override; virtual void SetIsRunningReducedWork(USkeletalMeshComponentBudgeted* Component, bool bInReducedWork) override; @@ -206,6 +207,12 @@ protected: /** The number of work units queued for tick this frame, used to calculate target AverageWorkUnitTimeMs. Updated each tick */ float NumWorkUnitsForAverage; + /** Budget pressure value, smoothed to reduce noise in 'reduced work' calculations */ + float SmoothedBudgetPressure; + + /** Throttle counter for delaying reduced work */ + int32 ReducedComponentWorkCounter; + /** Handle used to track garbage collection */ FDelegateHandle PostGarbageCollectHandle; diff --git a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/SkeletalMeshComponentBudgeted.cpp b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/SkeletalMeshComponentBudgeted.cpp index 72a04ee76bd6..54a8152fd885 100644 --- a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/SkeletalMeshComponentBudgeted.cpp +++ b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Private/SkeletalMeshComponentBudgeted.cpp @@ -3,6 +3,9 @@ #include "SkeletalMeshComponentBudgeted.h" #include "AnimationBudgetAllocator.h" #include "Kismet/KismetSystemLibrary.h" +#include "ProfilingDebugging/CsvProfiler.h" + +CSV_DECLARE_CATEGORY_EXTERN(AnimationBudget); FOnCalculateSignificance USkeletalMeshComponentBudgeted::OnCalculateSignificanceDelegate; @@ -39,6 +42,8 @@ void USkeletalMeshComponentBudgeted::EndPlay(const EEndPlayReason::Type EndPlayR void USkeletalMeshComponentBudgeted::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) { + CSV_SCOPED_TIMING_STAT(AnimationBudget, BudgetedAnimation); + if(AnimationBudgetAllocator) { uint64 StartTime = FPlatformTime::Cycles64(); @@ -55,6 +60,8 @@ void USkeletalMeshComponentBudgeted::TickComponent(float DeltaTime, enum ELevelT void USkeletalMeshComponentBudgeted::CompleteParallelAnimationEvaluation(bool bDoPostAnimEvaluation) { + CSV_SCOPED_TIMING_STAT(AnimationBudget, BudgetedAnimation); + if(AnimationBudgetAllocator) { uint64 StartTime = FPlatformTime::Cycles64(); diff --git a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Public/IAnimationBudgetAllocator.h b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Public/IAnimationBudgetAllocator.h index 54580d180ab5..d75466489bf7 100644 --- a/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Public/IAnimationBudgetAllocator.h +++ b/Engine/Plugins/Runtime/AnimationBudgetAllocator/Source/AnimationBudgetAllocator/Public/IAnimationBudgetAllocator.h @@ -46,6 +46,9 @@ public: /** Set the specified component to tick or not. If the budgeter is disabled then this calls Component->SetComponentTickEnabled(bShouldTick). */ virtual void SetComponentTickEnabled(USkeletalMeshComponentBudgeted* Component, bool bShouldTick) = 0; + /** Get whether the specified component is set to tick or not */ + virtual bool IsComponentTickEnabled(USkeletalMeshComponentBudgeted* Component) const = 0; + /** Inform that we reduced work for a component */ virtual void SetIsRunningReducedWork(USkeletalMeshComponentBudgeted* Component, bool bInReducedWork) = 0; diff --git a/Engine/Plugins/Runtime/AppleMoviePlayer/Source/AppleMoviePlayer/Private/AppleMovieStreamer.cpp b/Engine/Plugins/Runtime/AppleMoviePlayer/Source/AppleMoviePlayer/Private/AppleMovieStreamer.cpp index 3d07de1bb653..4f6818ec3bc4 100644 --- a/Engine/Plugins/Runtime/AppleMoviePlayer/Source/AppleMoviePlayer/Private/AppleMovieStreamer.cpp +++ b/Engine/Plugins/Runtime/AppleMoviePlayer/Source/AppleMoviePlayer/Private/AppleMovieStreamer.cpp @@ -29,7 +29,11 @@ static FString ConvertToNativePath(const FString& Filename, bool bForWrite) if(bForWrite) { +#if FILESHARING_ENABLED + static FString WritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/"); +#else static FString WritePathBase = FString([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]) + TEXT("/"); +#endif return WritePathBase + Result; } else diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/Abilities/GameplayAbility.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/Abilities/GameplayAbility.cpp index 720ecb75dc16..e79835f95b30 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/Abilities/GameplayAbility.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/Abilities/GameplayAbility.cpp @@ -1768,8 +1768,11 @@ void UGameplayAbility::BP_RemoveGameplayEffectFromOwnerWithAssetTags(FGameplayTa return; } - FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyEffectTags(WithTags); - CurrentActorInfo->AbilitySystemComponent->RemoveActiveEffects(Query, StacksToRemove); + if (CurrentActorInfo) + { + FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyEffectTags(WithTags); + CurrentActorInfo->AbilitySystemComponent->RemoveActiveEffects(Query, StacksToRemove); + } } void UGameplayAbility::BP_RemoveGameplayEffectFromOwnerWithGrantedTags(FGameplayTagContainer WithGrantedTags, int32 StacksToRemove) @@ -1779,8 +1782,11 @@ void UGameplayAbility::BP_RemoveGameplayEffectFromOwnerWithGrantedTags(FGameplay return; } - FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(WithGrantedTags); - CurrentActorInfo->AbilitySystemComponent->RemoveActiveEffects(Query, StacksToRemove); + if (CurrentActorInfo) + { + FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(WithGrantedTags); + CurrentActorInfo->AbilitySystemComponent->RemoveActiveEffects(Query, StacksToRemove); + } } void UGameplayAbility::BP_RemoveGameplayEffectFromOwnerWithHandle(FActiveGameplayEffectHandle Handle, int32 StacksToRemove) @@ -1790,7 +1796,10 @@ void UGameplayAbility::BP_RemoveGameplayEffectFromOwnerWithHandle(FActiveGamepla return; } - CurrentActorInfo->AbilitySystemComponent->RemoveActiveGameplayEffect(Handle, StacksToRemove); + if (CurrentActorInfo) + { + CurrentActorInfo->AbilitySystemComponent->RemoveActiveGameplayEffect(Handle, StacksToRemove); + } } float UGameplayAbility::GetCooldownTimeRemaining() const diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemBlueprintLibrary.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemBlueprintLibrary.cpp index ff0a17832ca7..b75852fdd179 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemBlueprintLibrary.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemBlueprintLibrary.cpp @@ -278,7 +278,7 @@ TArray UAbilitySystemBlueprintLibrary::GetActorsFromTargetData(const FG { if (TargetData.Data.IsValidIndex(Index)) { - FGameplayAbilityTargetData* Data = TargetData.Data[Index].Get(); + const FGameplayAbilityTargetData* Data = TargetData.Data[Index].Get(); TArray ResolvedArray; if (Data) { @@ -293,6 +293,27 @@ TArray UAbilitySystemBlueprintLibrary::GetActorsFromTargetData(const FG return TArray(); } +TArray UAbilitySystemBlueprintLibrary::GetAllActorsFromTargetData(const FGameplayAbilityTargetDataHandle& TargetData) +{ + TArray Result; + for (int32 TargetDataIndex = 0; TargetDataIndex < TargetData.Data.Num(); ++TargetDataIndex) + { + if (TargetData.Data.IsValidIndex(TargetDataIndex)) + { + const FGameplayAbilityTargetData* DataAtIndex = TargetData.Data[TargetDataIndex].Get(); + if (DataAtIndex) + { + TArray> WeakArray = DataAtIndex->GetActors(); + for (TWeakObjectPtr& WeakPtr : WeakArray) + { + Result.Add(WeakPtr.Get()); + } + } + } + } + return Result; +} + bool UAbilitySystemBlueprintLibrary::DoesTargetDataContainActor(const FGameplayAbilityTargetDataHandle& TargetData, int32 Index, AActor* Actor) { if (TargetData.Data.IsValidIndex(Index)) diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp index 44681b116184..97b748337e08 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent_Abilities.cpp @@ -156,6 +156,10 @@ void UAbilitySystemComponent::InitAbilityActorInfo(AActor* InOwnerActor, AActor* } LocalAnimMontageInfo = FGameplayAbilityLocalAnimMontage(); + if (IsOwnerActorAuthoritative()) + { + RepAnimMontageInfo = FGameplayAbilityRepAnimMontage(); + } if (bPendingMontageRep) { @@ -331,6 +335,15 @@ void UAbilitySystemComponent::ClearAbility(const FGameplayAbilitySpecHandle& Han check(IsOwnerActorAuthoritative()); // Should be called on authority bIsNetDirty = true; + for (int Idx = 0; Idx < AbilityPendingAdds.Num(); ++Idx) + { + if (AbilityPendingAdds[Idx].Handle == Handle) + { + AbilityPendingAdds.RemoveAtSwap(Idx, 1, false); + return; + } + } + for (int Idx = 0; Idx < ActivatableAbilities.Items.Num(); ++Idx) { check(ActivatableAbilities.Items[Idx].Handle.IsValid()); @@ -425,6 +438,22 @@ void UAbilitySystemComponent::OnRemoveAbility(FGameplayAbilitySpec& Spec) return; } + for (const FAbilityTriggerData& TriggerData : Spec.Ability->AbilityTriggers) + { + FGameplayTag EventTag = TriggerData.TriggerTag; + + auto& TriggeredAbilityMap = (TriggerData.TriggerSource == EGameplayAbilityTriggerSource::GameplayEvent) ? GameplayEventTriggeredAbilities : OwnedTagTriggeredAbilities; + + if (TriggeredAbilityMap.Contains(EventTag)) + { + TriggeredAbilityMap[EventTag].Remove(Spec.Handle); + if (TriggeredAbilityMap[EventTag].Num() == 0) + { + TriggeredAbilityMap.Remove(EventTag); + } + } + } + TArray Instances = Spec.GetAbilityInstances(); for (auto Instance : Instances) @@ -1772,7 +1801,7 @@ void UAbilitySystemComponent::ClientActivateAbilitySucceedWithEventData_Implemen bool UAbilitySystemComponent::TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle Handle, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag EventTag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component) { FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle); - if (!ensure(Spec)) + if (!ensureMsgf(Spec, TEXT("Failed to find gameplay ability spec %s"), *EventTag.ToString())) { return false; } @@ -2356,6 +2385,12 @@ float UAbilitySystemComponent::PlayMontage(UGameplayAbility* InAnimatingAbility, // Update parameters that change during Montage life time. AnimMontage_UpdateReplicatedData(); + + // Force net update on our avatar actor + if (AbilityActorInfo->AvatarActor != nullptr) + { + AbilityActorInfo->AvatarActor->ForceNetUpdate(); + } } else { diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AttributeSet.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AttributeSet.cpp index 4a4141bb682d..996427a92e14 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AttributeSet.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AttributeSet.cpp @@ -594,9 +594,9 @@ void FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData(const TArrayRowMap.CreateConstIterator(); It; ++It) + for (const TPair& CurveRow : CurTable->GetRowMap()) { - FString RowName = It.Key().ToString(); + FString RowName = CurveRow.Key.ToString(); FString ClassName; FString SetName; FString AttributeName; @@ -629,20 +629,21 @@ void FAttributeSetInitterDiscreteLevels::PreloadAttributeSetData(const TArrayGetLastKey().Time; + int32 LastLevel = Curve->GetKeyTime(Curve->GetLastKeyHandle()); DefaultCollection.LevelData.SetNum(FMath::Max(LastLevel, DefaultCollection.LevelData.Num())); //At this point we know the Name of this "class"/"group", the AttributeSet, and the Property Name. Now loop through the values on the curve to get the attribute default value at each level. - for (auto KeyIter = Curve->GetKeyIterator(); KeyIter; ++KeyIter) + for (auto KeyIter = Curve->GetKeyHandleIterator(); KeyIter; ++KeyIter) { - const FRichCurveKey& CurveKey = *KeyIter; + const FKeyHandle& KeyHandle = *KeyIter; - int32 Level = CurveKey.Time; - float Value = CurveKey.Value; + TPair LevelValuePair = Curve->GetKeyTimeValuePair(KeyHandle); + int32 Level = LevelValuePair.Key; + float Value = LevelValuePair.Value; FAttributeSetDefaults& SetDefaults = DefaultCollection.LevelData[Level-1]; diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayCueManager.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayCueManager.cpp index f0cc468fe892..65c72780a782 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayCueManager.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayCueManager.cpp @@ -69,7 +69,15 @@ void UGameplayCueManager::OnCreated() FNetworkReplayDelegates::OnPreScrub.AddUObject(this, &UGameplayCueManager::OnPreReplayScrub); #if WITH_EDITOR - FCoreDelegates::OnFEngineLoopInitComplete.AddUObject(this, &UGameplayCueManager::OnEngineInitComplete); + if (GIsRunning) + { + // Engine init already completed + OnEngineInitComplete(); + } + else + { + FCoreDelegates::OnFEngineLoopInitComplete.AddUObject(this, &UGameplayCueManager::OnEngineInitComplete); + } #endif } diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cpp index 9ef95ae62a42..23fd8aa66f0c 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/GameplayEffect.cpp @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "GameplayEffect.h" #include "TimerManager.h" @@ -30,6 +30,16 @@ const float UGameplayEffect::INVALID_LEVEL = FGameplayEffectConstants::INVALID_L DECLARE_CYCLE_STAT(TEXT("MakeQuery"), STAT_MakeGameplayEffectQuery, STATGROUP_AbilitySystem); +#if WITH_EDITOR +#define GETCURVE_REPORTERROR_WITHPOSTLOAD(Handle) \ + if (Handle.CurveTable) const_cast(Handle.CurveTable)->ConditionalPostLoad(); \ + GETCURVE_REPORTERROR(Handle); + +#define GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(Handle, PathNameString) \ + if (Handle.CurveTable) const_cast(Handle.CurveTable)->ConditionalPostLoad(); \ + GETCURVE_REPORTERROR_WITHPATHNAME(Handle, PathNameString); +#endif // WITH_EDITOR + // -------------------------------------------------------------------------------------------------------------------------------------------------------- // // UGameplayEffect @@ -89,8 +99,8 @@ void UGameplayEffect::PostLoad() HasGrantedApplicationImmunityQuery = !GrantedApplicationImmunityQuery.IsEmpty(); #if WITH_EDITOR - GETCURVE_REPORTERROR(Period.Curve); - GETCURVE_REPORTERROR(ChanceToApplyToTarget.Curve); + GETCURVE_REPORTERROR_WITHPOSTLOAD(Period.Curve); + GETCURVE_REPORTERROR_WITHPOSTLOAD(ChanceToApplyToTarget.Curve); DurationMagnitude.ReportErrors(GetPathName()); #endif // WITH_EDITOR @@ -567,19 +577,19 @@ void FGameplayEffectModifierMagnitude::ReportErrors(const FString& PathName) con { if (MagnitudeCalculationType == EGameplayEffectMagnitudeCalculation::ScalableFloat) { - GETCURVE_REPORTERROR_WITHPATHNAME(ScalableFloatMagnitude.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(ScalableFloatMagnitude.Curve, PathName); } else if (MagnitudeCalculationType == EGameplayEffectMagnitudeCalculation::AttributeBased) { - GETCURVE_REPORTERROR_WITHPATHNAME(AttributeBasedMagnitude.Coefficient.Curve, PathName); - GETCURVE_REPORTERROR_WITHPATHNAME(AttributeBasedMagnitude.PreMultiplyAdditiveValue.Curve, PathName); - GETCURVE_REPORTERROR_WITHPATHNAME(AttributeBasedMagnitude.PostMultiplyAdditiveValue.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(AttributeBasedMagnitude.Coefficient.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(AttributeBasedMagnitude.PreMultiplyAdditiveValue.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(AttributeBasedMagnitude.PostMultiplyAdditiveValue.Curve, PathName); } else if (MagnitudeCalculationType == EGameplayEffectMagnitudeCalculation::CustomCalculationClass) { - GETCURVE_REPORTERROR_WITHPATHNAME(CustomMagnitude.Coefficient.Curve, PathName); - GETCURVE_REPORTERROR_WITHPATHNAME(CustomMagnitude.PreMultiplyAdditiveValue.Curve, PathName); - GETCURVE_REPORTERROR_WITHPATHNAME(CustomMagnitude.PostMultiplyAdditiveValue.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(CustomMagnitude.Coefficient.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(CustomMagnitude.PreMultiplyAdditiveValue.Curve, PathName); + GETCURVE_REPORTERROR_WITHPATHNAME_WITHPOSTLOAD(CustomMagnitude.PostMultiplyAdditiveValue.Curve, PathName); } } #endif // WITH_EDITOR @@ -2030,37 +2040,13 @@ void FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom(FGameplayEffectSp void FActiveGameplayEffectsContainer::ExecutePeriodicGameplayEffect(FActiveGameplayEffectHandle Handle) { GAMEPLAYEFFECT_SCOPE_LOCK(); + FActiveGameplayEffect* ActiveEffect = GetActiveGameplayEffect(Handle); - if (ActiveEffect && !ActiveEffect->bIsInhibited) + + if (ActiveEffect != nullptr) { - FScopeCurrentGameplayEffectBeingApplied ScopedGEApplication(&ActiveEffect->Spec, Owner); - - if (UE_LOG_ACTIVE(VLogAbilitySystem, Log)) - { - ABILITY_VLOG(Owner->OwnerActor, Log, TEXT("Executed Periodic Effect %s"), *ActiveEffect->Spec.Def->GetFName().ToString()); - for (FGameplayModifierInfo Modifier : ActiveEffect->Spec.Def->Modifiers) - { - float Magnitude = 0.f; - Modifier.ModifierMagnitude.AttemptCalculateMagnitude(ActiveEffect->Spec, Magnitude); - ABILITY_VLOG(Owner->OwnerActor, Log, TEXT(" %s: %s %f"), *Modifier.Attribute.GetName(), *EGameplayModOpToString(Modifier.ModifierOp), Magnitude); - } - } - - // Clear modified attributes before each periodic execution - ActiveEffect->Spec.ModifiedAttributes.Empty(); - - // Execute - ExecuteActiveEffectsFrom(ActiveEffect->Spec); - - // Invoke Delegates for periodic effects being executed - UAbilitySystemComponent* SourceASC = ActiveEffect->Spec.GetContext().GetInstigatorAbilitySystemComponent(); - Owner->OnPeriodicGameplayEffectExecuteOnSelf(SourceASC, ActiveEffect->Spec, Handle); - if (SourceASC) - { - SourceASC->OnPeriodicGameplayEffectExecuteOnTarget(Owner, ActiveEffect->Spec, Handle); - } + InternalExecutePeriodicGameplayEffect(*ActiveEffect); } - } FActiveGameplayEffect* FActiveGameplayEffectsContainer::GetActiveGameplayEffect(const FActiveGameplayEffectHandle Handle) @@ -2596,29 +2582,36 @@ void FActiveGameplayEffectsContainer::SetAttributeBaseValue(FGameplayAttribute A float FActiveGameplayEffectsContainer::GetAttributeBaseValue(FGameplayAttribute Attribute) const { float BaseValue = 0.f; - const FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Attribute); - // if this attribute is of type FGameplayAttributeData then use the base value stored there - if (FGameplayAttribute::IsGameplayAttributeDataProperty(Attribute.GetUProperty())) + if (Owner) { - const UStructProperty* StructProperty = Cast(Attribute.GetUProperty()); - check(StructProperty); - const UAttributeSet* AttributeSet = Owner->GetAttributeSubobject(Attribute.GetAttributeSetClass()); - ensure(AttributeSet); - const FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr(AttributeSet); - if (DataPtr) + const FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Attribute); + // if this attribute is of type FGameplayAttributeData then use the base value stored there + if (FGameplayAttribute::IsGameplayAttributeDataProperty(Attribute.GetUProperty())) { - BaseValue = DataPtr->GetBaseValue(); + const UStructProperty* StructProperty = Cast(Attribute.GetUProperty()); + check(StructProperty); + const UAttributeSet* AttributeSet = Owner->GetAttributeSubobject(Attribute.GetAttributeSetClass()); + ensure(AttributeSet); + const FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr(AttributeSet); + if (DataPtr) + { + BaseValue = DataPtr->GetBaseValue(); + } + } + // otherwise, if we have an aggregator use the base value in the aggregator + else if (RefPtr) + { + BaseValue = RefPtr->Get()->GetBaseValue(); + } + // if the attribute is just a float and there is no aggregator then the base value is the current value + else + { + BaseValue = Owner->GetNumericAttribute(Attribute); } } - // otherwise, if we have an aggregator use the base value in the aggregator - else if (RefPtr) - { - BaseValue = RefPtr->Get()->GetBaseValue(); - } - // if the attribute is just a float and there is no aggregator then the base value is the current value else { - BaseValue = Owner->GetNumericAttribute(Attribute); + UE_LOG(LogGameplayEffects, Warning, TEXT("No Owner for FActiveGameplayEffectsContainer in GetAttributeBaseValue")); } return BaseValue; @@ -3176,6 +3169,40 @@ bool FActiveGameplayEffectsContainer::RemoveActiveGameplayEffect(FActiveGameplay return false; } +void FActiveGameplayEffectsContainer::InternalExecutePeriodicGameplayEffect(FActiveGameplayEffect& ActiveEffect) +{ + GAMEPLAYEFFECT_SCOPE_LOCK(); + if (!ActiveEffect.bIsInhibited) + { + FScopeCurrentGameplayEffectBeingApplied ScopedGEApplication(&ActiveEffect.Spec, Owner); + + if (UE_LOG_ACTIVE(VLogAbilitySystem, Log)) + { + ABILITY_VLOG(Owner->OwnerActor, Log, TEXT("Executed Periodic Effect %s"), *ActiveEffect.Spec.Def->GetFName().ToString()); + for (FGameplayModifierInfo Modifier : ActiveEffect.Spec.Def->Modifiers) + { + float Magnitude = 0.f; + Modifier.ModifierMagnitude.AttemptCalculateMagnitude(ActiveEffect.Spec, Magnitude); + ABILITY_VLOG(Owner->OwnerActor, Log, TEXT(" %s: %s %f"), *Modifier.Attribute.GetName(), *EGameplayModOpToString(Modifier.ModifierOp), Magnitude); + } + } + + // Clear modified attributes before each periodic execution + ActiveEffect.Spec.ModifiedAttributes.Empty(); + + // Execute + ExecuteActiveEffectsFrom(ActiveEffect.Spec); + + // Invoke Delegates for periodic effects being executed + UAbilitySystemComponent* SourceASC = ActiveEffect.Spec.GetContext().GetInstigatorAbilitySystemComponent(); + Owner->OnPeriodicGameplayEffectExecuteOnSelf(SourceASC, ActiveEffect.Spec, ActiveEffect.Handle); + if (SourceASC) + { + SourceASC->OnPeriodicGameplayEffectExecuteOnTarget(Owner, ActiveEffect.Spec, ActiveEffect.Handle); + } + } +} + /** Called by server to actually remove a GameplayEffect */ bool FActiveGameplayEffectsContainer::InternalRemoveActiveGameplayEffect(int32 Idx, int32 StacksToRemove, bool bPrematureRemoval) { @@ -3597,7 +3624,14 @@ void FActiveGameplayEffectsContainer::OnOwnerTagChange(FGameplayTag TagChange, i GAMEPLAYEFFECT_SCOPE_LOCK(); FGameplayTagContainer OwnerTags; - Owner->GetOwnedGameplayTags(OwnerTags); + if (Owner) + { + Owner->GetOwnedGameplayTags(OwnerTags); + } + else + { + UE_LOG(LogGameplayEffects, Warning, TEXT("No Owner for FActiveGameplayEffectsContainer in OnOwnerTagChange")); + } TSet& Handles = *Ptr; for (const FActiveGameplayEffectHandle& Handle : Handles) @@ -3809,21 +3843,11 @@ void FActiveGameplayEffectsContainer::CheckDuration(FActiveGameplayEffectHandle float PeriodTimeRemaining = TimerManager.GetTimerRemaining(Effect.PeriodHandle); if (PeriodTimeRemaining <= KINDA_SMALL_NUMBER && !Effect.bIsInhibited) { - FScopeCurrentGameplayEffectBeingApplied ScopedGEApplication(&Effect.Spec, Owner); + InternalExecutePeriodicGameplayEffect(Effect); - ExecuteActiveEffectsFrom(Effect.Spec); - - // Invoke Delegates for periodic effects being executed - UAbilitySystemComponent* SourceASC = Effect.Spec.GetContext().GetInstigatorAbilitySystemComponent(); - Owner->OnPeriodicGameplayEffectExecuteOnSelf(SourceASC, Effect.Spec, Handle); - if (SourceASC) - { - SourceASC->OnPeriodicGameplayEffectExecuteOnTarget(Owner, Effect.Spec, Handle); - } - - // The above call to ExecuteActiveEffectsFrom could cause this effect to be explicitly removed + // The call to ExecuteActiveEffectsFrom in InternalExecutePeriodicGameplayEffect could cause this effect to be explicitly removed // (for example it could kill the owner and cause the effect to be wiped via death). - // In that case, we need to early out instead of possibly continueing to the below calls to InternalRemoveActiveGameplayEffect + // In that case, we need to early out instead of possibly continuing to the below calls to InternalRemoveActiveGameplayEffect if ( Effect.IsPendingRemove ) { break; diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemBlueprintLibrary.h b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemBlueprintLibrary.h index 72e0026450ae..89efbc24ad1a 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemBlueprintLibrary.h +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemBlueprintLibrary.h @@ -122,10 +122,14 @@ class GAMEPLAYABILITIES_API UAbilitySystemBlueprintLibrary : public UBlueprintFu UFUNCTION(BlueprintPure, Category = "Spec") static FGameplayEffectSpecHandle CloneSpecHandle(AActor* InNewInstigator, AActor* InEffectCauser, FGameplayEffectSpecHandle GameplayEffectSpecHandle_Clone); - /** Returns all actors targeted */ + /** Returns all actors targeted, for a given index */ UFUNCTION(BlueprintPure, Category = "Ability|TargetData") static TArray GetActorsFromTargetData(const FGameplayAbilityTargetDataHandle& TargetData, int32 Index); + /** Returns all actors targeted */ + UFUNCTION(BlueprintPure, Category = "Ability|TargetData") + static TArray GetAllActorsFromTargetData(const FGameplayAbilityTargetDataHandle& TargetData); + /** Returns true if the given TargetData has the actor passed in targeted */ UFUNCTION(BlueprintPure, Category = "Ability|TargetData") static bool DoesTargetDataContainActor(const FGameplayAbilityTargetDataHandle& TargetData, int32 Index, AActor* Actor); diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h index 40b9fe26dded..ac5b78ce20db 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h @@ -73,7 +73,7 @@ DECLARE_MULTICAST_DELEGATE_TwoParams(FImmunityBlockGE, const FGameplayEffectSpec UENUM() enum class EGameplayEffectReplicationMode : uint8 { - /** Only replicate minimal gameplay effect info*/ + /** Only replicate minimal gameplay effect info. Note: this does not work for Owned AbilitySystemComponents (Use Mixed instead). */ Minimal, /** Only replicate minimal gameplay effect info to simulated proxies but full info to owners and autonomous proxies */ Mixed, diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.h b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.h index 5070c61016ed..94af254a861f 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.h +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AttributeSet.h @@ -338,8 +338,8 @@ public: private: - // Cached direct pointer to RichCurve we should evaluate - mutable FRichCurve* FinalCurve; + // Cached direct pointer to the RealCurve we should evaluate + mutable FRealCurve* FinalCurve; }; template<> diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.h b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.h index 47823548e4b8..838b8fde0d47 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.h +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayEffect.h @@ -1717,6 +1717,8 @@ private: return OwnerIsNetAuthority; } + void InternalExecutePeriodicGameplayEffect(FActiveGameplayEffect& ActiveEffect); + /** Called internally to actually remove a GameplayEffect or to reduce its StackCount. Returns true if we resized our internal GameplayEffect array. */ bool InternalRemoveActiveGameplayEffect(int32 Idx, int32 StacksToRemove, bool bPrematureRemoval); diff --git a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilitiesEditor/Private/AttributeDetails.cpp b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilitiesEditor/Private/AttributeDetails.cpp index 791411b35d17..6e4025d860fb 100644 --- a/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilitiesEditor/Private/AttributeDetails.cpp +++ b/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilitiesEditor/Private/AttributeDetails.cpp @@ -427,7 +427,7 @@ TSharedPtr FScalableFloatDetails::InitWidgetContent() if (CurveTable != NULL) { /** Extract all the row names from the RowMap */ - for (TMap::TConstIterator Iterator(CurveTable->RowMap); Iterator; ++Iterator) + for (TMap::TConstIterator Iterator(CurveTable->GetRowMap()); Iterator; ++Iterator) { /** Create a simple array of the row names */ TSharedRef RowNameItem = MakeShareable(new FString(Iterator.Key().ToString())); @@ -601,7 +601,7 @@ void FScalableFloatDetails::OnFilterTextChanged(const FText& InFilterText) if (CurveTable != NULL) { /** Extract all the row names from the RowMap */ - for (TMap::TConstIterator Iterator(CurveTable->RowMap); Iterator; ++Iterator) + for (TMap::TConstIterator Iterator(CurveTable->GetRowMap()); Iterator; ++Iterator) { /** Create a simple array of the row names */ FString RowString = Iterator.Key().ToString(); diff --git a/Engine/Plugins/Runtime/GoogleARCore/Source/GoogleARCoreRendering/Public/GoogleARCorePassthroughCameraRenderer.h b/Engine/Plugins/Runtime/GoogleARCore/Source/GoogleARCoreRendering/Public/GoogleARCorePassthroughCameraRenderer.h index 0066f57ceb79..0aa04c2967d2 100644 --- a/Engine/Plugins/Runtime/GoogleARCore/Source/GoogleARCoreRendering/Public/GoogleARCorePassthroughCameraRenderer.h +++ b/Engine/Plugins/Runtime/GoogleARCore/Source/GoogleARCoreRendering/Public/GoogleARCorePassthroughCameraRenderer.h @@ -28,7 +28,7 @@ class GOOGLEARCORERENDERING_API UGoogleARCoreCameraOverlayMaterialLoader : publi public: /** A pointer to the camera overlay material that will be used to render the passthrough camera texture as background. */ UPROPERTY() - UMaterialInterface* DefaultCameraOverlayMaterial; + UMaterialInterface* DefaultCameraOverlayMaterial; UGoogleARCoreCameraOverlayMaterialLoader() { diff --git a/Engine/Plugins/Runtime/LeapMotion/Source/LeapMotion/Public/AnimBody/AnimBone.h b/Engine/Plugins/Runtime/LeapMotion/Source/LeapMotion/Public/AnimBody/AnimBone.h index 0fb40ac1b39c..e498d061c971 100644 --- a/Engine/Plugins/Runtime/LeapMotion/Source/LeapMotion/Public/AnimBody/AnimBone.h +++ b/Engine/Plugins/Runtime/LeapMotion/Source/LeapMotion/Public/AnimBody/AnimBone.h @@ -30,11 +30,11 @@ class UAnimBone : public UObject //Optional vector of the next joint (outward) UPROPERTY(BlueprintReadWrite, Category = "Anim Bone") - FVector NextJoint; + FVector NextJoint; //Optional vector of the previous joint (inward) UPROPERTY(BlueprintReadWrite, Category = "Anim Bone") - FVector PrevJoint; + FVector PrevJoint; UFUNCTION(BlueprintCallable, Category = "Anim Bone") bool Enabled(); diff --git a/Engine/Plugins/Runtime/LocationServicesBPLibrary/Source/LocationServicesBPLibrary/Classes/LocationServicesBPLibrary.h b/Engine/Plugins/Runtime/LocationServicesBPLibrary/Source/LocationServicesBPLibrary/Classes/LocationServicesBPLibrary.h index f3e05718bbeb..ce62a0db9951 100644 --- a/Engine/Plugins/Runtime/LocationServicesBPLibrary/Source/LocationServicesBPLibrary/Classes/LocationServicesBPLibrary.h +++ b/Engine/Plugins/Runtime/LocationServicesBPLibrary/Source/LocationServicesBPLibrary/Classes/LocationServicesBPLibrary.h @@ -123,7 +123,7 @@ public: * @return - true if the mobile device can support the Accuracy, false if it will use a different accuracy */ UFUNCTION(BlueprintCallable, Category = "Services|Mobile|Location") - static bool IsLocationAccuracyAvailable(ELocationAccuracy Accuracy); + static bool IsLocationAccuracyAvailable(ELocationAccuracy Accuracy); /* * Set and clear the Location Services implementation object. Used by the module at startup and shutdown, not intended diff --git a/Engine/Plugins/Runtime/Nvidia/Ansel/Source/Ansel/Public/AnselFunctionLibrary.h b/Engine/Plugins/Runtime/Nvidia/Ansel/Source/Ansel/Public/AnselFunctionLibrary.h index a136f19854fe..dc59a9a7249c 100644 --- a/Engine/Plugins/Runtime/Nvidia/Ansel/Source/Ansel/Public/AnselFunctionLibrary.h +++ b/Engine/Plugins/Runtime/Nvidia/Ansel/Source/Ansel/Public/AnselFunctionLibrary.h @@ -22,57 +22,57 @@ class ANSEL_API UAnselFunctionLibrary : public UBlueprintFunctionLibrary public: /** Starts a photography session */ UFUNCTION(BlueprintCallable, Category = "Photography", meta = (WorldContext = WorldContextObject)) - static void StartSession(UObject* WorldContextObject); + static void StartSession(UObject* WorldContextObject); /** Stops a photography session */ UFUNCTION(BlueprintCallable, Category = "Photography", meta = (WorldContext = WorldContextObject)) - static void StopSession(UObject* WorldContextObject); + static void StopSession(UObject* WorldContextObject); /** Whether the photography system is available at all. See CVar r.Photography.Available */ UFUNCTION(BlueprintCallable, Category = "Photography") - static bool IsPhotographyAvailable(); + static bool IsPhotographyAvailable(); /** Whether the app is permitting photography at this time. See CVar r.Photography.Allowed */ UFUNCTION(BlueprintCallable, Category = "Photography") - static bool IsPhotographyAllowed(); + static bool IsPhotographyAllowed(); /** Sets whether the app is permitting photography at this time. See CVar r.Photography.Allowed */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetIsPhotographyAllowed(const bool bIsPhotographyAllowed); + static void SetIsPhotographyAllowed(const bool bIsPhotographyAllowed); /** Sets the number of frames between captures in a multi-part shot. See CVar r.Photography.SettleFrames */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetSettleFrames(const int NumSettleFrames); + static void SetSettleFrames(const int NumSettleFrames); /** Sets the normal speed of movement of the photography camera. See CVar r.Photography.TranslationSpeed */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetCameraMovementSpeed(const float TranslationSpeed); + static void SetCameraMovementSpeed(const float TranslationSpeed); /** Sets the size of the photography camera for collision purposes; only relevant when default implementation of PlayerCameraManager's PhotographyCameraModify function is used. See CVar r.Photography.Constrain.CameraSize */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetCameraConstraintCameraSize(const float CameraSize); + static void SetCameraConstraintCameraSize(const float CameraSize); /** Sets maximum distance which the camera is allowed to wander from its initial position; only relevant when default implementation of PlayerCameraManager's PhotographyCameraModify function is used. See CVar r.Photography.Constrain.MaxCameraDistance */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetCameraConstraintDistance(const float MaxCameraDistance); + static void SetCameraConstraintDistance(const float MaxCameraDistance); /** Sets whether the photography system automatically tries to optimize Unreal's postprocessing effects for photography. See CVar r.Photography.AutoPostprocess */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetAutoPostprocess(const bool bShouldAutoPostprocess); + static void SetAutoPostprocess(const bool bShouldAutoPostprocess); /** Sets whether the photography system automatically pauses the game during a photography session. See CVar r.Photography.AutoPause */ UFUNCTION(BlueprintCallable, Category = "Photography") - static void SetAutoPause(const bool bShouldAutoPause); + static void SetAutoPause(const bool bShouldAutoPause); /** Show or hide controls in the photography UI which let the player tweak standard UE visual effects during photography - for example, depth of field or chromatic aberration. Note: these controls only exist when SetAutoPostprocess is turned on. Some may not apply to your application either because you are not using the associated effect or you are using a custom version of the effect. */ UFUNCTION(BlueprintCallable, Category = "Photography", meta = (WorldContext = WorldContextObject)) - static void SetUIControlVisibility(UObject* WorldContextObject, const TEnumAsByte UIControlTarget, const bool bIsVisible); + static void SetUIControlVisibility(UObject* WorldContextObject, const TEnumAsByte UIControlTarget, const bool bIsVisible); /** A utility which constrains distance of camera from its start point; may be useful when implementing a custom APlayerCameraManager::PhotographyCameraModify */ UFUNCTION(BlueprintCallable, Category = "Photography", meta = (WorldContext = WorldContextObject)) - static void ConstrainCameraByDistance(UObject* WorldContextObject, const FVector NewCameraLocation, const FVector PreviousCameraLocation, const FVector OriginalCameraLocation, FVector& OutCameraLocation, float MaxDistance); + static void ConstrainCameraByDistance(UObject* WorldContextObject, const FVector NewCameraLocation, const FVector PreviousCameraLocation, const FVector OriginalCameraLocation, FVector& OutCameraLocation, float MaxDistance); /** A utility which constrains the camera against collidable geometry; may be useful when implementing a custom APlayerCameraManager::PhotographyCameraModify */ UFUNCTION(BlueprintCallable, Category = "Photography", meta = (WorldContext = WorldContextObject)) - static void ConstrainCameraByGeometry(UObject* WorldContextObject, const FVector NewCameraLocation, const FVector PreviousCameraLocation, const FVector OriginalCameraLocation, FVector& OutCameraLocation); + static void ConstrainCameraByGeometry(UObject* WorldContextObject, const FVector NewCameraLocation, const FVector PreviousCameraLocation, const FVector OriginalCameraLocation, FVector& OutCameraLocation); }; diff --git a/Engine/Plugins/Runtime/ProceduralMeshComponent/Source/ProceduralMeshComponent/Public/KismetProceduralMeshLibrary.h b/Engine/Plugins/Runtime/ProceduralMeshComponent/Source/ProceduralMeshComponent/Public/KismetProceduralMeshLibrary.h index 87fc1ec83ba8..e231d18249dc 100644 --- a/Engine/Plugins/Runtime/ProceduralMeshComponent/Source/ProceduralMeshComponent/Public/KismetProceduralMeshLibrary.h +++ b/Engine/Plugins/Runtime/ProceduralMeshComponent/Source/ProceduralMeshComponent/Public/KismetProceduralMeshLibrary.h @@ -63,7 +63,7 @@ class PROCEDURALMESHCOMPONENT_API UKismetProceduralMeshLibrary : public UBluepri * @param GridSpacing Size of each quad in world units */ UFUNCTION(BlueprintCallable, Category = "Components|ProceduralMesh") - static void CreateGridMeshWelded(int32 NumX, int32 NumY, TArray& Triangles, TArray& Vertices, TArray& UVs, float GridSpacing = 16.0f); + static void CreateGridMeshWelded(int32 NumX, int32 NumY, TArray& Triangles, TArray& Vertices, TArray& UVs, float GridSpacing = 16.0f); /** * Generate a vertex buffer, index buffer and UVs for a grid mesh where each quad is split, with standard 0-1 UVs on UV0 and point sampled texel center UVs for UV1. @@ -76,7 +76,7 @@ class PROCEDURALMESHCOMPONENT_API UKismetProceduralMeshLibrary : public UBluepri * @param GridSpacing Size of each quad in world units */ UFUNCTION(BlueprintCallable, Category = "Components|ProceduralMesh") - static void CreateGridMeshSplit(int32 NumX, int32 NumY, TArray& Triangles, TArray& Vertices, TArray& UVs, TArray& UV1s, float GridSpacing = 16.0f); + static void CreateGridMeshSplit(int32 NumX, int32 NumY, TArray& Triangles, TArray& Vertices, TArray& UVs, TArray& UV1s, float GridSpacing = 16.0f); /** Grab geometry data from a StaticMesh asset. */ UFUNCTION(BlueprintCallable, Category = "Components|ProceduralMesh") diff --git a/Engine/Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp b/Engine/Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp index dc8e9fc27870..b98f0af1263e 100644 --- a/Engine/Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp +++ b/Engine/Plugins/Runtime/ReplicationGraph/Source/Private/ReplicationGraph.cpp @@ -59,6 +59,7 @@ #include "Net/DataChannel.h" #include "UObject/UObjectGlobals.h" #include "DrawDebugHelpers.h" +#include "Misc/ScopeExit.h" int32 CVar_RepGraph_Pause = 0; static FAutoConsoleVariableRef CVarRepGraphPause(TEXT("Net.RepGraph.Pause"), CVar_RepGraph_Pause, TEXT("Pauses actor replication in the Replication Graph."), ECVF_Default ); @@ -87,6 +88,9 @@ static FAutoConsoleVariableRef CVarRepGraphTrackClassReplication(TEXT("Net.RepGr int32 CVar_RepGraph_PrintTrackClassReplication = 0; static FAutoConsoleVariableRef CVarRepGraphPrintTrackClassReplication(TEXT("Net.RepGraph.PrintTrackClassReplication"), CVar_RepGraph_PrintTrackClassReplication, TEXT(""), ECVF_Default ); +int32 CVar_RepGraph_DormantDynamicActorsDestruction = 1; +static FAutoConsoleVariableRef CVarRepGraphDormantDynamicActorsDestruction(TEXT("Net.RepGraph.DormantDynamicActorsDestruction"), CVar_RepGraph_DormantDynamicActorsDestruction, TEXT(""), ECVF_Default ); + REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.LogNetDormancyDetails", CVar_RepGraph_LogNetDormancyDetails, 0, "Logs actors that are removed from the replication graph/nodes."); REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.LogActorRemove", CVar_RepGraph_LogActorRemove, 0, "Logs actors that are removed from the replication graph/nodes."); REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.LogActorAdd", CVar_RepGraph_LogActorAdd, 0, "Logs actors that are added to replication graph/nodes."); @@ -96,6 +100,7 @@ REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.DisableBandwithLimit", CVar_RepG REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.TrickleDistCullOnDormanyNodes", CVar_RepGraph_TrickleDistCullOnDormanyNodes, 1, "Actors in a dormancy node that are distance culled will trickle through as dormancy node empties"); REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.EnableRPCSendPolicy", CVar_RepGraph_EnableRPCSendPolicy, 1, "Enables RPC send policy (e.g, force certain functions to send immediately rather than be queued)"); REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.EnableFastSharedPath", CVar_RepGraph_EnableFastSharedPath, 1, "Enables FastShared replication path for lists with EActorRepListTypeFlags::FastShared flag"); +REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.EnableDynamicAllocationWarnings", CVar_RepGraph_EnableDynamicAllocationWarnings, 1, "Enables debug information whenever RepGraph needs to allocate new Actor Lists."); DECLARE_STATS_GROUP(TEXT("ReplicationDriver"), STATGROUP_RepDriver, STATCAT_Advanced); DECLARE_DWORD_COUNTER_STAT(TEXT("Rep Actor List Dupes"), STAT_NetRepActorListDupes, STATGROUP_RepDriver); @@ -123,7 +128,7 @@ FORCEINLINE bool RepGraphConditionalActorBreakpoint(AActor* Actor, UNetConnectio } // Alternatively, DebugActorConnectionPair can be set by code to catch a specific actor/connection pair - if (DebugActorConnectionPair.Actor.Get() == Actor && DebugActorConnectionPair.Connection == NetConnection) + if (DebugActorConnectionPair.Actor.Get() == Actor && (DebugActorConnectionPair.Connection == nullptr || DebugActorConnectionPair.Connection == NetConnection )) { return true; } @@ -187,15 +192,18 @@ UReplicationGraph::UReplicationGraph() { OnListRequestExceedsPooledSize = [](int32 NewExpectedSize) { - FReplicationGraphDebugInfo DebugInfo(*GLog); - DebugInfo.Flags = FReplicationGraphDebugInfo::ShowNativeClasses; - - for (TObjectIterator It; It; ++It) + if (CVar_RepGraph_EnableDynamicAllocationWarnings) { - It->LogGraph(DebugInfo); - } + FReplicationGraphDebugInfo DebugInfo(*GLog); + DebugInfo.Flags = FReplicationGraphDebugInfo::ShowNativeClasses; - ensureAlwaysMsgf(false, TEXT("Very large replication list size requested. NewExpectedSize: %d"), NewExpectedSize); + for (TObjectIterator It; It; ++It) + { + It->LogGraph(DebugInfo); + } + + ensureAlwaysMsgf(false, TEXT("Very large replication list size requested. NewExpectedSize: %d"), NewExpectedSize); + } }; } #endif @@ -476,6 +484,11 @@ void UReplicationGraph::RemoveNetworkActor(AActor* Actor) } GlobalActorReplicationInfoMap.Remove(Actor); + + for (UNetReplicationGraphConnection* ConnectionManager : Connections) + { + ConnectionManager->ActorInfoMap.RemoveActor(Actor); + } } void UReplicationGraph::RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo) @@ -513,7 +526,7 @@ void UReplicationGraph::FlushNetDormancy(AActor* Actor, bool bWasDormInitial) if (GlobalInfo.bWantsToBeDormant != bNewWantsToBeDormant) { - UE_LOG(LogReplicationGraph, Display, TEXT("UReplicationGraph::FlushNetDormancy %s. WantsToBeDormant is changing (%d -> %d) from a Flush! We expect NotifyActorDormancyChange to be called first."), *Actor->GetPathName(), (bool)GlobalInfo.bWantsToBeDormant, bNewWantsToBeDormant); + UE_LOG(LogReplicationGraph, Verbose, TEXT("UReplicationGraph::FlushNetDormancy %s. WantsToBeDormant is changing (%d -> %d) from a Flush! We expect NotifyActorDormancyChange to be called first."), *Actor->GetPathName(), (bool)GlobalInfo.bWantsToBeDormant, bNewWantsToBeDormant); GlobalInfo.bWantsToBeDormant = Actor->NetDormancy > DORM_Awake; } @@ -681,7 +694,15 @@ int32 UReplicationGraph::ServerReplicateActors(float DeltaSeconds) SCOPED_NAMED_EVENT(UReplicationGraph_ServerReplicateActors, FColor::Green); ++NetDriver->ReplicationFrame; // This counter is used by RepLayout to utilize CL/serialization sharing. We must increment it ourselves, but other places can increment it too, in order to invalidate the shared state. - const uint32 FrameNum = ++ReplicationGraphFrame; // This counter is used internally and drives all frame based replication logic. + const uint32 FrameNum = ReplicationGraphFrame; // This counter is used internally and drives all frame based replication logic. + + ON_SCOPE_EXIT + { + // We increment this after our replication has happened. If we increment at the beginning of this function, then we rep with FrameNum X, then start the next game frame with the same FrameNum X. If at the top of that frame, + // when processing packets, ticking, etc, we get calls to TearOff, ForceNetUpdate etc which make use of ReplicationGraphFrame, they will be using a stale frame num. So we could replicate, get a server move next frame, ForceNetUpdate, but think we + // already replicated this frame. + ReplicationGraphFrame++; + }; // ------------------------------------------------------- // PREPARE (Global) @@ -779,7 +800,7 @@ int32 UReplicationGraph::ServerReplicateActors(float DeltaSeconds) FConnectionReplicationActorInfo& ConnectionActorInfo = *MapIt.Value().Get(); UActorChannel* Channel = MapIt.Key(); checkSlow(Channel != nullptr); - ensure(Channel == ConnectionActorInfo.Channel); + ensureMsgf(Channel == ConnectionActorInfo.Channel, TEXT("Channel: %s ConnectionActorInfo.Channel: %s. Actor: %s "), *GetNameSafe(Channel), *GetNameSafe(ConnectionActorInfo.Channel), ConnectionActorInfo.Channel ? *GetNameSafe(ConnectionActorInfo.Channel->Actor) : TEXT("NULLCHANNEL")); if (ConnectionActorInfo.ActorChannelCloseFrameNum > 0 && ConnectionActorInfo.ActorChannelCloseFrameNum <= FrameNum) { @@ -808,7 +829,7 @@ int32 UReplicationGraph::ServerReplicateActors(float DeltaSeconds) } INC_DWORD_STAT_BY( STAT_NetActorChannelsClosed, 1 ); - ConnectionActorInfo.Channel->Close(); + ConnectionActorInfo.Channel->Close(EChannelCloseReason::Relevancy); } } } @@ -822,6 +843,14 @@ int32 UReplicationGraph::ServerReplicateActors(float DeltaSeconds) ConnectionManager->ReplicateDestructionInfos(ConnectionViewLocation, DestructInfoMaxDistanceSquared); } + // ------------------------------------------ + // Handle Dormant Destruction Infos. These are actors that are dormant but no longer relevant to the client. + // ------------------------------------------ + { + QUICK_SCOPE_CYCLE_COUNTER(NET_ReplicateActors_ReplicateDormantDestructionInfos); + ConnectionManager->ReplicateDormantDestructionInfos(); + } + #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) { RG_QUICK_SCOPE_CYCLE_COUNTER(NET_ReplicateActors_ReplicateDebugActor); @@ -1009,6 +1038,18 @@ void UReplicationGraph::ReplicateActorListsForConnection_Default(UNetReplication } } + // ------------------------ + // Pending dormancy scaling + // ------------------------ + + // Make sure pending dormant actors that have replicated at least once are prioritized, + // so we actually mark them dormant quickly, skip future work, and close their channels. + // Otherwise, newly spawned or never-replicated actors may starve out existing actors trying to go dormant. + if (GlobalData.bWantsToBeDormant && ConnectionData.LastRepFrameNum > 0) + { + AccumulatedPriority -= 1.5f; + } + // ------------------- // Game code priority // ------------------- @@ -1244,13 +1285,27 @@ void UReplicationGraph::ReplicateActorListsForConnection_FastShared(UNetReplicat } } + +REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.FastShared.ForceFull", CVar_RepGraph_FastShared_ForceFull, 0, "Redirects calls to ReplicateSingleActor_FastShared to ReplicateSingleActor"); + int64 UReplicationGraph::ReplicateSingleActor_FastShared(AActor* Actor, FConnectionReplicationActorInfo& ConnectionData, FGlobalActorReplicationInfo& GlobalActorInfo, UNetConnection* NetConnection, const uint32 FrameNum) { - int32 BitsWritten = 0; - FScopedFastPathTracker ScopedTracker(Actor->GetClass(), CSVTracker, BitsWritten); // Track time and bandwidth for this class + // No matter what we consider this FastShared rep to happen. Even if the actor doesn't produce a bunch or its empty or stale, etc. We still consider this replication to have happened + // for high level frequency purposes (E.g, UReplicationGraphNode_DynamicSpatialFrequency). But we want to do the update at the end of this function, not at the top since it can early out + // if the actor doesn't produce a new bunch and this connection already got the last bunch produced. + ON_SCOPE_EXIT + { + ConnectionData.FastPath_LastRepFrameNum = FrameNum; + ConnectionData.FastPath_NextReplicationFrameNum = FrameNum + ConnectionData.FastPath_ReplicationPeriodFrame; + }; + + if (CVar_RepGraph_FastShared_ForceFull > 0) + { + return ReplicateSingleActor(Actor, ConnectionData, GlobalActorInfo, FindOrAddConnectionManager(NetConnection)->ActorInfoMap, NetConnection, FrameNum); + } - ConnectionData.FastPath_LastRepFrameNum = FrameNum; - ConnectionData.FastPath_NextReplicationFrameNum = FrameNum + ConnectionData.FastPath_ReplicationPeriodFrame; + int32 BitsWritten = 0; + FScopedFastPathTracker ScopedTracker(Actor->GetClass(), CSVTracker, BitsWritten); // Track time and bandwidth for this class UActorChannel* ActorChannel = ConnectionData.Channel; @@ -1268,9 +1323,9 @@ int64 UReplicationGraph::ReplicateSingleActor_FastShared(AActor* Actor, FConnect FOutBunch& OutBunch = GlobalActorInfo.FastSharedReplicationInfo->Bunch; // Update the shared bunch if its out of date - if (GlobalActorInfo.FastSharedReplicationInfo->LastBuiltFrameNum < FrameNum) + if (GlobalActorInfo.FastSharedReplicationInfo->LastAttemptBuildFrameNum < FrameNum) { - GlobalActorInfo.FastSharedReplicationInfo->LastBuiltFrameNum = FrameNum; + GlobalActorInfo.FastSharedReplicationInfo->LastAttemptBuildFrameNum = FrameNum; if (GlobalActorInfo.Settings.FastSharedReplicationFunc == nullptr) { @@ -1289,8 +1344,6 @@ int64 UReplicationGraph::ReplicateSingleActor_FastShared(AActor* Actor, FConnect FastSharedReplicationBunch = &OutBunch; FastSharedReplicationChannel = ActorChannel; - FastSharedReplicationBunch->Reset(); - // Calling this function *should* result in an RPC call that we trap and fill out FastSharedReplicationBunch. See UReplicationGraph::ProcessRemoteFunction if (GlobalActorInfo.Settings.FastSharedReplicationFunc(Actor) == false) { @@ -1300,13 +1353,26 @@ int64 UReplicationGraph::ReplicateSingleActor_FastShared(AActor* Actor, FConnect return 0; } - if (!ensureMsgf(FastSharedReplicationBunch == nullptr, TEXT("FastSharedReplicationBunch Not cleared after calling FastSharedReplicationFunc on %s"), *Actor->GetPathName())) + + if (FastSharedReplicationBunch == nullptr) { + // A new bunch was produced this frame. (FastSharedReplicationBunch is cleared in ::ProcessRemoteFunction) + GlobalActorInfo.FastSharedReplicationInfo->LastBunchBuildFrameNum = FrameNum; + } + else + { + // A new bunch was not produced this frame, but there is still valid data (If FastSharedReplicationFunc returns false, there is no valid data) FastSharedReplicationBunch = nullptr; FastSharedReplicationChannel = nullptr; } } + if (ConnectionData.FastPath_LastRepFrameNum >= GlobalActorInfo.FastSharedReplicationInfo->LastBunchBuildFrameNum) + { + // We already repped this bunch to this connection. So just return + return 0; + } + if (OutBunch.GetNumBits() <= 0) { // Empty bunch - no need to send. This means we aren't fast repping this guy this frame @@ -1395,7 +1461,7 @@ int64 UReplicationGraph::ReplicateSingleActor(AActor* Actor, FConnectionReplicat { // Replicate and immediately close in tear off case BitsWritten = ActorInfo.Channel->ReplicateActor(); - BitsWritten += ActorInfo.Channel->Close(); + BitsWritten += ActorInfo.Channel->Close(EChannelCloseReason::TearOff); } else { @@ -1542,6 +1608,9 @@ bool UReplicationGraph::ProcessRemoteFunction(class AActor* Actor, UFunction* Fu // into a static function. if (ensureMsgf(FastSharedReplicationChannel, TEXT("FastSharedReplicationPath set but FastSharedReplicationChannel is not! %s"), *Actor->GetPathName())) { + // Reset the bunch here. It will be reused and we should only reset it right before we actually write to it. + FastSharedReplicationBunch->Reset(); + // It sucks we have to a temp writer like this, but we don't know how big the payload will be until we serialize it FNetBitWriter TempWriter(nullptr, 0); TSharedPtr RepLayout = NetDriver->GetFunctionRepLayout( Function ); @@ -1891,6 +1960,25 @@ void UNetReplicationGraphConnection::NotifyAddDestructionInfo(FActorDestructionI //UE_LOG(LogReplicationGraph, Display, TEXT("::NotifyAddDestructionInfo. Connection: %s. DestructInfo: %s. NewTotal: %d"), *NetConnection->Describe(), *DestructInfo->PathName, PendingDestructInfoList.Num()); } +void UNetReplicationGraphConnection::NotifyAddDormantDestructionInfo(AActor* Actor) +{ + if (NetConnection && NetConnection->Driver && NetConnection->Driver->GuidCache) + { + FNetworkGUID NetGUID = NetConnection->Driver->GuidCache->GetNetGUID(Actor); + if (NetGUID.IsValid() && !NetGUID.IsDefault()) + { + PendingDormantDestructList.RemoveAll([NetGUID](const FCachedDormantDestructInfo& Info) { return (Info.NetGUID == NetGUID); }); + + FCachedDormantDestructInfo& Info = PendingDormantDestructList.AddDefaulted_GetRef(); + + Info.NetGUID = NetGUID; + Info.Level = Actor->GetLevel(); + Info.ObjOuter = Actor->GetOuter(); + Info.PathName = Actor->GetName(); + } + } +} + void UNetReplicationGraphConnection::NotifyRemoveDestructionInfo(FActorDestructionInfo* DestructInfo) { int32 RemoveIdx = PendingDestructInfoList.IndexOfByKey(DestructInfo); @@ -1980,6 +2068,35 @@ int64 UNetReplicationGraphConnection::ReplicateDestructionInfos(const FVector& C return NumBits; } +int64 UNetReplicationGraphConnection::ReplicateDormantDestructionInfos() +{ + CSV_SCOPED_TIMING_STAT_EXCLUSIVE(ReplicateDormantDestructionInfos); + + int64 NumBits = 0; + + for (const FCachedDormantDestructInfo& Info : PendingDormantDestructList) + { + FActorDestructionInfo DestructInfo; + DestructInfo.DestroyedPosition = FVector::ZeroVector; + DestructInfo.NetGUID = Info.NetGUID; + DestructInfo.Level = Info.Level; + DestructInfo.ObjOuter = Info.ObjOuter; + DestructInfo.PathName = Info.PathName; + DestructInfo.StreamingLevelName = NAME_None; // currently unused by SetChannelActorForDestroy + DestructInfo.Reason = EChannelCloseReason::Relevancy; + + UActorChannel* Channel = (UActorChannel*)NetConnection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally); + if (Channel) + { + NumBits += Channel->SetChannelActorForDestroy(&DestructInfo); + } + } + + PendingDormantDestructList.Empty(); + + return NumBits; +} + // -------------------------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------------------------------- @@ -2491,7 +2608,6 @@ FORCEINLINE bool ReplicatesEveryFrame(const FConnectionReplicationActorInfo& Con void UReplicationGraphNode_DynamicSpatialFrequency::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) { - repCheck(GraphGlobals.IsValid()); UReplicationGraph* RepGraph = GraphGlobals->ReplicationGraph; @@ -2682,8 +2798,9 @@ void UReplicationGraphNode_DynamicSpatialFrequency::GatherActorListsForConnectio } REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.DynamicSpatialFrequency.Draw", CVar_RepGraph_DynamicSpatialFrequency_Draw, 0, ""); +REPGRAPH_DEVCVAR_SHIPCONST(int32, "Net.RepGraph.DynamicSpatialFrequency.ForceMaxFreq", CVar_RepGraph_DynamicSpatialFrequency_ForceMaxFreq, 0, "Forces DSF to set max frame replication periods on all actors (1 frame rep periods). 1 = default replication. 2 = fast path. 3 = Both (effectively, default)"); -FORCEINLINE uint32 CalcDynamicReplicationPeriod(const float FinalPCT, const uint32 MinRepPeriod, const uint32 MaxRepPeriod, uint8& OutReplicationPeriodFrame, uint32& OutNextReplicationFrame, const uint32 LastRepFrameNum, const uint32 FrameNum) +FORCEINLINE uint32 CalcDynamicReplicationPeriod(const float FinalPCT, const uint32 MinRepPeriod, const uint32 MaxRepPeriod, uint8& OutReplicationPeriodFrame, uint32& OutNextReplicationFrame, const uint32 LastRepFrameNum, const uint32 FrameNum, bool ForFastPath) { const float PeriodRange = (float)(MaxRepPeriod - MinRepPeriod); const uint32 ExtraPeriod = (uint32)FMath::CeilToInt(PeriodRange * FinalPCT); @@ -2694,11 +2811,28 @@ FORCEINLINE uint32 CalcDynamicReplicationPeriod(const float FinalPCT, const uint const uint32 NextRepFrameNum = LastRepFrameNum + FinalPeriod; OutNextReplicationFrame = NextRepFrameNum; + +#if !(UE_BUILD_SHIPPING) + if (CVar_RepGraph_DynamicSpatialFrequency_ForceMaxFreq > 0) + { + if ((CVar_RepGraph_DynamicSpatialFrequency_ForceMaxFreq == 1 && ForFastPath == 0) || + (CVar_RepGraph_DynamicSpatialFrequency_ForceMaxFreq == 2 && ForFastPath == 1) || + CVar_RepGraph_DynamicSpatialFrequency_ForceMaxFreq == 3 + ) + { + OutReplicationPeriodFrame = 1; + OutNextReplicationFrame = FrameNum; + } + } +#endif + return ExtraPeriod; } static TArray DynamicSpatialFrequencyDebugColorArray = { FColor::Red, FColor::Green, FColor::Blue, FColor::Cyan, FColor::Orange, FColor::Purple }; + + FORCEINLINE void UReplicationGraphNode_DynamicSpatialFrequency::CalcFrequencyForActor(AActor* Actor, UReplicationGraph* RepGraph, UNetConnection* NetConnection, FGlobalActorReplicationInfo& GlobalInfo, FConnectionReplicationActorInfo& ConnectionInfo, FSettings& MySettings, const FVector& ConnectionViewLocation, const FVector& ConnectionViewDir, const uint32 FrameNum, int32 ExistingItemIndex) { // If we need to filter out the actor and he is already in the SortedReplicationList, we need to remove it (instead of just skipping/returning). @@ -2765,7 +2899,6 @@ FORCEINLINE void UReplicationGraphNode_DynamicSpatialFrequency::CalcFrequencyFor // -------------------------------------------------------------------------------------------------------- { // Calc Percentage of distance relative to cull distance, scaled to ZoneInfo Min/Max pct - const float CullDistSq = ConnectionInfo.CullDistanceSquared > 0.f ? ConnectionInfo.CullDistanceSquared : GlobalInfo.Settings.CullDistanceSquared; // Use global settings if the connection specific setting is zero'd out if (!ensureMsgf(CullDistSq > 0.f, TEXT("UReplicationGraphNode_DynamicSpatialFrequency::GatherActors: %s has cull distance of 0. Skipping"), *GetPathNameSafe(Actor))) @@ -2782,7 +2915,7 @@ FORCEINLINE void UReplicationGraphNode_DynamicSpatialFrequency::CalcFrequencyFor const float FinalPCT = FMath::Clamp( BiasDistPct / (ZoneInfo.MaxDistPct - ZoneInfo.MinDistPct), 0.f, 1.f); // Calc Replication period for Normal replication - CalcDynamicReplicationPeriod(FinalPCT, ZoneInfo.MinRepPeriod, ZoneInfo.MaxRepPeriod, ConnectionInfo.ReplicationPeriodFrame, ConnectionInfo.NextReplicationFrameNum, ConnectionInfo.LastRepFrameNum, FrameNum); + CalcDynamicReplicationPeriod(FinalPCT, ZoneInfo.MinRepPeriod, ZoneInfo.MaxRepPeriod, ConnectionInfo.ReplicationPeriodFrame, ConnectionInfo.NextReplicationFrameNum, ConnectionInfo.LastRepFrameNum, FrameNum, false); FramesTillReplicate = (int32)ConnectionInfo.NextReplicationFrameNum - (int32)FrameNum; // Update actor timeout frame here in case we get starved and can't actually replicate before then @@ -2791,7 +2924,7 @@ FORCEINLINE void UReplicationGraphNode_DynamicSpatialFrequency::CalcFrequencyFor // Calc Replication Period for FastShared replication if (ActorSupportsFastShared && ZoneInfo.FastPath_MinRepPeriod > 0) { - CalcDynamicReplicationPeriod(FinalPCT, ZoneInfo.FastPath_MinRepPeriod, ZoneInfo.FastPath_MaxRepPeriod, ConnectionInfo.FastPath_ReplicationPeriodFrame, ConnectionInfo.FastPath_NextReplicationFrameNum, ConnectionInfo.FastPath_LastRepFrameNum, FrameNum); + CalcDynamicReplicationPeriod(FinalPCT, ZoneInfo.FastPath_MinRepPeriod, ZoneInfo.FastPath_MaxRepPeriod, ConnectionInfo.FastPath_ReplicationPeriodFrame, ConnectionInfo.FastPath_NextReplicationFrameNum, ConnectionInfo.FastPath_LastRepFrameNum, FrameNum, true); FramesTillReplicate = FMath::Min(FramesTillReplicate, (int32)ConnectionInfo.FastPath_NextReplicationFrameNum - (int32)FrameNum); EnableFastPath = true; } @@ -3141,7 +3274,7 @@ void UReplicationGraphNode_DormancyNode::RemoveDormantActor(const FNewReplicated for (auto& MapIt : ConnectionNodes) { UReplicationGraphNode_ConnectionDormanyNode* Node = MapIt.Value; - Node->NotifyRemoveNetworkActor(ActorInfo, false); // Don't warn if not found, the node may have removed the actor itself. Not wortht he extra bookkeeping to skip the call. + Node->NotifyRemoveNetworkActor(ActorInfo, false); // Don't warn if not found, the node may have removed the actor itself. Not worth the extra bookkeeping to skip the call. } } @@ -3152,6 +3285,12 @@ void UReplicationGraphNode_DormancyNode::GatherActorListsForConnection(const FCo return; } + UReplicationGraphNode_ConnectionDormanyNode* ConnectionNode = GetConnectionNode(Params); + ConnectionNode->GatherActorListsForConnection(Params); +} + +UReplicationGraphNode_ConnectionDormanyNode* UReplicationGraphNode_DormancyNode::GetConnectionNode(const FConnectionGatherActorListParameters& Params) +{ UReplicationGraphNode_ConnectionDormanyNode** NodePtrPtr = ConnectionNodes.Find(&Params.ConnectionManager); UReplicationGraphNode_ConnectionDormanyNode* ConnectionNode = nullptr; if (!NodePtrPtr) @@ -3170,7 +3309,7 @@ void UReplicationGraphNode_DormancyNode::GatherActorListsForConnection(const FCo ConnectionNode = *NodePtrPtr; } - ConnectionNode->GatherActorListsForConnection(Params); + return ConnectionNode; } void UReplicationGraphNode_DormancyNode::OnActorDormancyFlush(FActorRepListType Actor, FGlobalActorReplicationInfo& GlobalInfo) @@ -3203,6 +3342,30 @@ void UReplicationGraphNode_DormancyNode::OnActorDormancyFlush(FActorRepListType Node->NotifyActorDormancyFlush(Actor); } } + +void UReplicationGraphNode_DormancyNode::ConditionalGatherDormantDynamicActors(FActorRepListRefView& RepList, const FConnectionGatherActorListParameters& Params, FActorRepListRefView* RemovedList) +{ + for (FActorRepListType& Actor : ReplicationActorList) + { + if (Actor && !Actor->IsNetStartupActor()) + { + if (FConnectionReplicationActorInfo* Info = Params.ConnectionManager.ActorInfoMap.Find(Actor)) + { + if (Info->bDormantOnConnection) + { + if (RemovedList && RemovedList->IsValid() && RemovedList->Contains(Actor)) + { + continue; + } + + RepList.PrepareForWrite(); + RepList.ConditionalAdd(Actor); + } + } + } + } +} + // -------------------------------------------------------------------------------------------------------------------------------------------- @@ -3258,18 +3421,18 @@ void UReplicationGraphNode_GridCell::ConditionalCopyDormantActors(FActorRepListR { if (GraphGlobals.IsValid()) { - for (int32 idx = FromList.Num()-1; idx >= 0; --idx) - { - FActorRepListType Actor = FromList[idx]; + for (int32 idx = FromList.Num()-1; idx >= 0; --idx) + { + FActorRepListType Actor = FromList[idx]; FGlobalActorReplicationInfo& GlobalInfo = GraphGlobals->GlobalActorReplicationInfoMap->Get(Actor); if (GlobalInfo.bWantsToBeDormant) - { - ToNode->NotifyAddNetworkActor(FNewReplicatedActorInfo(Actor)); - FromList.RemoveAtSwap(idx); + { + ToNode->NotifyAddNetworkActor(FNewReplicatedActorInfo(Actor)); + FromList.RemoveAtSwap(idx); + } } } } -} void UReplicationGraphNode_GridCell::OnStaticActorNetDormancyChange(FActorRepListType Actor, FGlobalActorReplicationInfo& GlobalInfo, ENetDormancy NewValue, ENetDormancy OldValue) { @@ -4117,10 +4280,72 @@ void UReplicationGraphNode_GridSpatialization2D::GatherActorListsForConnection(c GridX.SetNum(CellY+1); } - if (UReplicationGraphNode_GridCell* Node = GridX[CellY]) + UReplicationGraphNode_GridCell* CellNode = GridX[CellY]; + if (CellNode) { - Node->GatherActorListsForConnection(Params); + CellNode->GatherActorListsForConnection(Params); } + + if (CVar_RepGraph_DormantDynamicActorsDestruction > 0) + { + int32 PrevX = FMath::Max(0, (int32)((Params.ConnectionManager.LastGatherLocation.X - SpatialBias.X) / CellSize)); + int32 PrevY = FMath::Max(0, (int32)((Params.ConnectionManager.LastGatherLocation.Y - SpatialBias.Y) / CellSize)); + + // if the grid cell changed this gather + if ((CellX != PrevX) || (CellY != PrevY)) + { + RG_QUICK_SCOPE_CYCLE_COUNTER(UReplicationGraphNode_GridSpatialization2D_CellChangeDormantRelevancy); + + FActorRepListRefView DormantActorList; + FActorRepListRefView PrevDormantActorList; + + if (CellNode) + { + if (UReplicationGraphNode_DormancyNode* DormancyNode = CellNode->GetDormancyNode()) + { + DormancyNode->ConditionalGatherDormantDynamicActors(DormantActorList, Params, nullptr); + } + } + + TArray& PrevGridX = GetGridX(PrevX); + if (UReplicationGraphNode_GridCell* PrevCell = GetCell(PrevGridX, PrevY)) + { + if (UReplicationGraphNode_DormancyNode* DormancyNode = PrevCell->GetDormancyNode()) + { + DormancyNode->ConditionalGatherDormantDynamicActors(PrevDormantActorList, Params, &DormantActorList); + } + } + + // any previous dormant actors not in the current node dormant list + if (PrevDormantActorList.IsValid()) + { + for (FActorRepListType& Actor : PrevDormantActorList) + { + Params.ConnectionManager.NotifyAddDormantDestructionInfo(Actor); + + if (FConnectionReplicationActorInfo* ActorInfo = Params.ConnectionManager.ActorInfoMap.Find(Actor)) + { + ActorInfo->bDormantOnConnection = false; + + // add back to connection specific dormancy nodes + const FActorCellInfo CellInfo = GetCellInfoForActor(Actor, Actor->GetActorLocation(), ActorInfo->CullDistanceSquared); + + GetGridNodesForActor(Actor, CellInfo, GatheredNodes); + + for (UReplicationGraphNode_GridCell* Node : GatheredNodes) + { + if (UReplicationGraphNode_DormancyNode* DormancyNode = Node->GetDormancyNode()) + { + DormancyNode->GetConnectionNode(Params)->NotifyActorDormancyFlush(Actor); + } + } + } + } + } + } + } + + Params.ConnectionManager.LastGatherLocation = Params.Viewer.ViewLocation; } void UReplicationGraphNode_GridSpatialization2D::NotifyActorCullDistChange(AActor* Actor, FGlobalActorReplicationInfo& GlobalInfo, float OldDistSq) @@ -4259,8 +4484,12 @@ void UReplicationGraphNode_TearOff_ForConnection::GatherActorListsForConnection( for (int32 idx=TearOffActors.Num()-1; idx >=0; --idx) { - AActor* Actor = TearOffActors[idx].Actor; - const uint32 TearOffFrameNum = TearOffActors[idx].TearOffFrameNum; + FTearOffActorInfo& TearOffInfo = TearOffActors[idx]; + + AActor* Actor = TearOffInfo.Actor; + const uint32 TearOffFrameNum = TearOffInfo.TearOffFrameNum; + + //UE_LOG(LogReplicationGraph, Display, TEXT("UReplicationGraphNode_TearOff_ForConnection::GatherActorListsForConnection. Actor: %s. GetTearOff: %d. FrameNum: %d. TearOffFrameNum: %d"), *GetNameSafe(Actor), (int32)Actor->GetTearOff(), Params.ReplicationFrameNum, TearOffFrameNum); // If actor is still valid (not pending kill etc) if (Actor && IsActorValidForReplication(Actor)) @@ -4268,15 +4497,22 @@ void UReplicationGraphNode_TearOff_ForConnection::GatherActorListsForConnection( // And has not replicated since becoming torn off if (FConnectionReplicationActorInfo* ActorInfo = ActorInfoMap.Find(Actor)) { - if (ActorInfo->LastRepFrameNum <= TearOffFrameNum) + //UE_LOG(LogReplicationGraph, Display, TEXT("0x%X ActorInfo->LastRepFrameNum: %d ActorInfo->NextReplicationFrameNum: %d. (%d)"), (int64)ActorInfo, ActorInfo->LastRepFrameNum, ActorInfo->NextReplicationFrameNum, (ActorInfo->NextReplicationFrameNum - Params.ReplicationFrameNum)); + + // Keep adding it to the out list until its replicated at least once. Saturation can prevent it from happening on any given frame. + // But we could also rep, get an ack for the close, clear the actor's ActorInfo (set LastRepFrameNum = 0), and "miss it". So track that here with bHasReppedOnce + if (ActorInfo->LastRepFrameNum <= TearOffFrameNum && !(ActorInfo->LastRepFrameNum <= 0 && TearOffInfo.bHasReppedOnce)) { // Add it to the rep list ReplicationActorList.Add(Actor); + TearOffInfo.bHasReppedOnce = true; continue; } } } + //UE_LOG(LogReplicationGraph, Display, TEXT("Removing tearOffActor: %s. GetTearOff: %d"), *GetNameSafe(Actor), (int32)Actor->GetTearOff()); + // If we didn't get added to the list, remove this TearOffActors.RemoveAtSwap(idx, 1, false); } @@ -4288,6 +4524,11 @@ void UReplicationGraphNode_TearOff_ForConnection::GatherActorListsForConnection( } } +void UReplicationGraphNode_TearOff_ForConnection::NotifyTearOffActor(AActor* Actor, uint32 FrameNum) +{ + TearOffActors.Emplace( Actor, FrameNum); +} + // ------------------------------------------------------- void UReplicationGraphNode_AlwaysRelevant_ForConnection::GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) diff --git a/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraph.h b/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraph.h index 696da273f23c..412db0922691 100644 --- a/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraph.h +++ b/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraph.h @@ -413,6 +413,10 @@ public: void OnActorDormancyFlush(FActorRepListType Actor, FGlobalActorReplicationInfo& GlobalInfo); + void ConditionalGatherDormantDynamicActors(FActorRepListRefView& RepList, const FConnectionGatherActorListParameters& Params, FActorRepListRefView* RemovedList); + + UReplicationGraphNode_ConnectionDormanyNode* GetConnectionNode(const FConnectionGatherActorListParameters& Params); + private: TMap ConnectionNodes; @@ -439,6 +443,8 @@ public: // Allow graph to override function for creating the dynamic node in the cell TFunction CreateDynamicNodeOverride; + UReplicationGraphNode_DormancyNode* GetDormancyNode(); + private: UPROPERTY() @@ -448,7 +454,6 @@ private: UReplicationGraphNode_DormancyNode* DormancyNode = nullptr; UReplicationGraphNode* GetDynamicNode(); - UReplicationGraphNode_DormancyNode* GetDormancyNode(); void OnActorDormancyFlush(FActorRepListType Actor, FGlobalActorReplicationInfo& GlobalInfo, UReplicationGraphNode_DormancyNode* DormancyNode ); @@ -669,13 +674,15 @@ USTRUCT() struct FTearOffActorInfo { GENERATED_BODY() - FTearOffActorInfo() : TearOffFrameNum(0), Actor(nullptr) { } - FTearOffActorInfo(AActor* InActor, uint32 InTearOffFrameNum) : TearOffFrameNum(InTearOffFrameNum), Actor(InActor) { } + FTearOffActorInfo() : TearOffFrameNum(0), Actor(nullptr), bHasReppedOnce(false) { } + FTearOffActorInfo(AActor* InActor, uint32 InTearOffFrameNum) : TearOffFrameNum(InTearOffFrameNum), Actor(InActor), bHasReppedOnce(false) { } uint32 TearOffFrameNum; UPROPERTY() AActor* Actor; + + bool bHasReppedOnce; }; /** Adds actors that are always relevant for a connection. This engine version just adds the PlayerController and ViewTarget (usually the pawn) */ @@ -692,7 +699,7 @@ public: virtual void GatherActorListsForConnection(const FConnectionGatherActorListParameters& Params) override; virtual void LogNode(FReplicationGraphDebugInfo& DebugInfo, const FString& NodeName) const override; - void NotifyTearOffActor(AActor* Actor, uint32 FrameNum) { TearOffActors.Emplace( Actor, FrameNum); } + void NotifyTearOffActor(AActor* Actor, uint32 FrameNum); // Fixme: not safe to have persistent FActorRepListrefViews yet, so need a uproperty based list to hold the persistent items. UPROPERTY() @@ -951,9 +958,13 @@ public: // ID that is assigned by the replication graph. Will be reassigned/compacted as clients disconnect. Useful for spacing out connection operations. E.g., not stable but always compact. int32 ConnectionId; + FVector LastGatherLocation; + /** Returns connection graph nodes. This is const so that you do not mutate the array itself. You should use AddConnectionGraphNode/RemoveConnectionGraphNode. */ const TArray& GetConnectionGraphNodes() { return ConnectionGraphNodes; } + virtual void NotifyAddDormantDestructionInfo(AActor* Actor) override; + private: friend UReplicationGraph; @@ -991,6 +1002,8 @@ private: bool PrepareForReplication(); int64 ReplicateDestructionInfos(const FVector& ConnectionViewLocation, const float DestructInfoMaxDistanceSquared); + + int64 ReplicateDormantDestructionInfos(); UPROPERTY() TArray ConnectionGraphNodes; @@ -1010,6 +1023,16 @@ private: TArray PendingDestructInfoList; TSet TrackedDestructionInfoPtrs; // Set used to guard against double adds into PendingDestructInfoList + + struct FCachedDormantDestructInfo + { + TWeakObjectPtr Level; + TWeakObjectPtr ObjOuter; + FNetworkGUID NetGUID; + FString PathName; + }; + + TArray PendingDormantDestructList; }; // -------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraphTypes.h b/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraphTypes.h index d827a3b0f581..d0dd9bafd1cb 100644 --- a/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraphTypes.h +++ b/Engine/Plugins/Runtime/ReplicationGraph/Source/Public/ReplicationGraphTypes.h @@ -401,7 +401,9 @@ struct FGlobalActorReplicationInfo; struct FFastSharedReplicationInfo { - uint32 LastBuiltFrameNum = 0; + // LastBuiltFrameNum = 0; + uint32 LastAttemptBuildFrameNum = 0; // the last frame we called FastSharedReplicationFunc on + uint32 LastBunchBuildFrameNum = 0; // the last frame a new bunch was actually created FOutBunch Bunch; }; @@ -1264,7 +1266,7 @@ private: FTrackedData(FString Suffix) { #if REPGRAPH_CSV_TRACKER - StatName = FName(*(FString(CSV_STAT_NAME_PREFIX) + Suffix)); + StatName = FName(*Suffix); #endif } diff --git a/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudio/Public/ResonanceAudioAmbisonicsSettings.h b/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudio/Public/ResonanceAudioAmbisonicsSettings.h index 5768c6d26c69..f82614e25b80 100644 --- a/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudio/Public/ResonanceAudioAmbisonicsSettings.h +++ b/Engine/Plugins/Runtime/ResonanceAudio/Source/ResonanceAudio/Public/ResonanceAudioAmbisonicsSettings.h @@ -24,5 +24,5 @@ class RESONANCEAUDIO_API UResonanceAudioAmbisonicsSettings : public UAmbisonicsS public: //Which order of ambisonics to use for this submix. UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Ambisonics) - EAmbisonicsOrder AmbisonicsOrder; + EAmbisonicsOrder AmbisonicsOrder; }; \ No newline at end of file diff --git a/Engine/Plugins/Runtime/SunPosition/Source/SunPosition/Public/SunPosition.h b/Engine/Plugins/Runtime/SunPosition/Source/SunPosition/Public/SunPosition.h index b9164b495c2f..1e2f619313a3 100644 --- a/Engine/Plugins/Runtime/SunPosition/Source/SunPosition/Public/SunPosition.h +++ b/Engine/Plugins/Runtime/SunPosition/Source/SunPosition/Public/SunPosition.h @@ -49,5 +49,5 @@ class SUNPOSITION_API USunPositionFunctionLibrary : public UBlueprintFunctionLib public: /** Get the sun's position data based on position, date and time */ UFUNCTION(BlueprintCallable, Category = "Sun Position") - static void GetSunPosition(float Latitude, float Longitude, float TimeZone, bool bIsDaylightSavingTime, int32 Year, int32 Month, int32 Day, int32 Hours, int32 Minutes, int32 Seconds, FSunPositionData& SunPositionData); + static void GetSunPosition(float Latitude, float Longitude, float TimeZone, bool bIsDaylightSavingTime, int32 Year, int32 Month, int32 Day, int32 Hours, int32 Minutes, int32 Seconds, FSunPositionData& SunPositionData); }; diff --git a/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Classes/SynthComponents/EpicSynth1Component.h b/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Classes/SynthComponents/EpicSynth1Component.h index 8502f74b0ec0..011e8087e8d9 100644 --- a/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Classes/SynthComponents/EpicSynth1Component.h +++ b/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Classes/SynthComponents/EpicSynth1Component.h @@ -66,7 +66,7 @@ struct SYNTHESIS_API FModularSynthPreset : public FTableRowBase // What type of oscillator to use for oscillator 2 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Synth|Preset") - ESynth1OscType Osc2Type; + ESynth1OscType Osc2Type; // The linear gain of oscillator 2 [0.0, 1.0] UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Synth|Preset", meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) diff --git a/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Private/UI/Synth2DSlider.cpp b/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Private/UI/Synth2DSlider.cpp index 2d94115e1c43..992b45688c60 100644 --- a/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Private/UI/Synth2DSlider.cpp +++ b/Engine/Plugins/Runtime/Synthesis/Source/Synthesis/Private/UI/Synth2DSlider.cpp @@ -150,7 +150,7 @@ void USynth2DSlider::SetSliderHandleColor(FLinearColor InValue) const FText USynth2DSlider::GetPaletteCategory() { - return LOCTEXT("Common", "Common"); + return LOCTEXT("Synth", "Synth"); } #endif diff --git a/Engine/Plugins/ScriptPlugin/Source/ScriptEditorPlugin/Private/ScriptBlueprintCompiler.cpp b/Engine/Plugins/ScriptPlugin/Source/ScriptEditorPlugin/Private/ScriptBlueprintCompiler.cpp index e226fb73ca4b..3aacd6db30be 100644 --- a/Engine/Plugins/ScriptPlugin/Source/ScriptEditorPlugin/Private/ScriptBlueprintCompiler.cpp +++ b/Engine/Plugins/ScriptPlugin/Source/ScriptEditorPlugin/Private/ScriptBlueprintCompiler.cpp @@ -61,6 +61,10 @@ void FScriptBlueprintCompiler::CreateClassVariablesFromBlueprint() { PinCategory = UEdGraphSchema_K2::PC_Int; } + else if (Field.Class->IsChildOf(UInt64Property::StaticClass())) + { + PinCategory = UEdGraphSchema_K2::PC_Int64; + } else if (Field.Class->IsChildOf(UBoolProperty::StaticClass())) { PinCategory = UEdGraphSchema_K2::PC_Boolean; diff --git a/Engine/Shaders/Private/ClearReplacementShaders.usf b/Engine/Shaders/Private/ClearReplacementShaders.usf index e5264edda00e..f7330f4cf28e 100644 --- a/Engine/Shaders/Private/ClearReplacementShaders.usf +++ b/Engine/Shaders/Private/ClearReplacementShaders.usf @@ -26,7 +26,7 @@ float4 ClearPS() : SV_Target0 return ClearColor; } -#if FEATURE_LEVEL >= FEATURE_LEVEL_SM5 +#if COMPUTESHADER RWTexture2D ClearTextureRW; uint4 TargetBounds; //xy upperleft, zw lowerright @@ -75,4 +75,4 @@ void ClearVolumeCS(uint3 Position : SV_DispatchThreadID) ClearVolumeRW[Position] = ClearColor; } -#endif +#endif // COMPUTESHADER diff --git a/Engine/Shaders/Private/LandscapeProceduralPS.usf b/Engine/Shaders/Private/LandscapeProceduralPS.usf new file mode 100644 index 000000000000..fc4074ec4a4a --- /dev/null +++ b/Engine/Shaders/Private/LandscapeProceduralPS.usf @@ -0,0 +1,322 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "Common.ush" + +Texture2D ReadHeightmapTexture1; +Texture2D ReadHeightmapTexture2; +SamplerState ReadHeightmapTextureSampler; +float2 LayerInfo; // x == weight, y == visibility +float4 OutputConfig; // x == ApplyLayerModifiers, y == OutputAsDelta, z == Use ReadHeightmapTexture2 and out Delta with ReadHeightmapTexture1, w == Output Normals +float2 HeightmapTextureSize; // x == source heightmap width, y == source heightmap height +float3 LandscapeGridScale; // x == LS Actor DrawScale.X, y == LS Actor DrawScale.y, z == LS Actor DrawScale.z / 128.0f (ZSCALE) +float CurrentMipComponentVertexCount; + +float3 SampleTextureYNormal(float2 InTextureCoordinates, float2 InTexelSize, bool InUp) +{ + float2 SamplingUV = InTextureCoordinates; + SamplingUV.y = InUp ? SamplingUV.y - InTexelSize.y : SamplingUV.y + InTexelSize.y; + float2 PosSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SamplingUV, 0).rg; + float PosHeight = float(((int)round(PosSample.r * 255.0) << 8) | (int)round(PosSample.g * 255.0)) - 32768.0; + return float3(SamplingUV * HeightmapTextureSize * LandscapeGridScale.xy, PosHeight * LandscapeGridScale.z * TERRAIN_ZSCALE); +} + +float3 SampleTextureXNormal(float2 InTextureCoordinates, float2 InTexelSize, bool InLeft) +{ + float2 SamplingUV = InTextureCoordinates; + SamplingUV.x = InLeft ? SamplingUV.x - InTexelSize.x : SamplingUV.x + InTexelSize.x; + float2 PosSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SamplingUV, 0).rg; + float PosHeight = float(((int)round(PosSample.r * 255.0) << 8) | (int)round(PosSample.g * 255.0)) - 32768.0; + return float3(SamplingUV * HeightmapTextureSize * LandscapeGridScale.xy, PosHeight * LandscapeGridScale.z * TERRAIN_ZSCALE); +} + +void PSMain(float2 InTextureCoordinates : TEXCOORD0, + out float4 OutColor : SV_Target0) +{ + float4 SourceColor = ReadHeightmapTexture1.Sample(ReadHeightmapTextureSampler, InTextureCoordinates); + float Height = float(((int)round(SourceColor.r * 255.0) << 8) | (int)round(SourceColor.g * 255.0)); + + // Perform calculation 0 based + Height -= 32768.0f; + + // Output as Layer, so apply Layer info + if (OutputConfig.x == 1.0) + { + Height = round(Height * LayerInfo.x * LayerInfo.y); + } + + // Output using 2nd heightmap + if (OutputConfig.z == 1.0) + { + float2 BaseColor = ReadHeightmapTexture2.Sample(ReadHeightmapTextureSampler, InTextureCoordinates).rg; + float BaseHeight = float(((int)round(BaseColor.r * 255.0) << 8) | (int)round(BaseColor.g * 255.0)); + + Height = OutputConfig.y == 1 ? BaseHeight + (Height - BaseHeight) : BaseHeight + Height; + } + else + { + // Output as Delta to Source + if (OutputConfig.y == 0.0) + { + Height += 32768.0f; + } + } + + Height = clamp(Height, 0.0f, 65536.0f); + int iHeight = (int)Height; + float2 PackedHeight = float2((float)((iHeight - (iHeight & 255)) >> 8) / 255.0, (float)(iHeight & 255) / 255.0); + + OutColor.r = PackedHeight.x; + OutColor.g = PackedHeight.y; + OutColor.b = SourceColor.b; + OutColor.a = SourceColor.a; + + // Output normals + if (OutputConfig.w == 1.0) + { + float2 TexelSize = 1.0 / HeightmapTextureSize; + + bool IsMinBorderTexelX = InTextureCoordinates.x <= TexelSize.x; + bool IsMinBorderTexelY = InTextureCoordinates.y <= TexelSize.y; + bool IsMaxBorderTexelX = InTextureCoordinates.x >= TexelSize.x * min(CurrentMipComponentVertexCount - 1, 1.0); + bool IsMaxBorderTexelY = InTextureCoordinates.y >= TexelSize.y * min(CurrentMipComponentVertexCount - 1, 1.0); + + float3 CurrentPos = float3(InTextureCoordinates * HeightmapTextureSize * LandscapeGridScale.xy, (Height - 32768.0f) * LandscapeGridScale.z * TERRAIN_ZSCALE); + float3 FinalNormal = 0.0; + + if (IsMinBorderTexelX) // left border + { + if (IsMinBorderTexelY) // top border + { + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + FinalNormal = normalize(cross(PosRight - CurrentPos, PosDown - CurrentPos)); + } + else if (IsMaxBorderTexelY) // bottom border + { + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + FinalNormal = normalize(cross(PosUp - CurrentPos, PosRight - CurrentPos)); + } + else + { + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + + float3 VectorUpRight = cross(PosUp - CurrentPos, PosRight - CurrentPos); + float3 VectorRightDown = cross(PosRight - CurrentPos, PosDown - CurrentPos); + FinalNormal = normalize(VectorUpRight + VectorRightDown); + } + } + else if (IsMaxBorderTexelX) // right border + { + if (IsMinBorderTexelY) // top border + { + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + FinalNormal = normalize(cross(PosDown - CurrentPos, PosLeft - CurrentPos)); + } + else if (IsMaxBorderTexelY) // bottom border + { + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + FinalNormal = normalize(cross(PosLeft - CurrentPos, PosUp - CurrentPos)); + } + else + { + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + + float3 VectorLeftUp = cross(PosLeft - CurrentPos, PosUp - CurrentPos); + float3 VectorDownLeft = cross(PosDown - CurrentPos, PosLeft - CurrentPos); + FinalNormal = normalize(VectorDownLeft + VectorLeftUp); + } + } + else // center border + { + if (IsMinBorderTexelY) // top border + { + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + + float3 VectorRightDown = cross(PosRight - CurrentPos, PosDown - CurrentPos); + float3 VectorDownLeft = cross(PosDown - CurrentPos, PosLeft - CurrentPos); + FinalNormal = normalize(VectorRightDown + VectorDownLeft); + } + else if (IsMaxBorderTexelY) // bottom border + { + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + + float3 VectorUpRight = cross(PosUp - CurrentPos, PosRight - CurrentPos); + float3 VectorLeftUp = cross(PosLeft - CurrentPos, PosUp - CurrentPos); + FinalNormal = normalize(VectorUpRight + VectorLeftUp); + } + else + { + float3 PosRight = SampleTextureXNormal(InTextureCoordinates, TexelSize, false); + float3 PosLeft = SampleTextureXNormal(InTextureCoordinates, TexelSize, true); + float3 PosUp = SampleTextureYNormal(InTextureCoordinates, TexelSize, true); + float3 PosDown = SampleTextureYNormal(InTextureCoordinates, TexelSize, false); + + float3 VectorRightDown = cross(PosRight - CurrentPos, PosDown - CurrentPos); + float3 VectorDownLeft = cross(PosDown - CurrentPos, PosLeft - CurrentPos); + + float3 VectorUpRight = cross(PosUp - CurrentPos, PosRight - CurrentPos); + float3 VectorLeftUp = cross(PosLeft - CurrentPos, PosUp - CurrentPos); + + FinalNormal = normalize(VectorUpRight + VectorLeftUp + VectorRightDown + VectorDownLeft); + } + } + + // Scale back to be 0-1 normal + OutColor.b = (FinalNormal.x + 1.0) * 0.5; + OutColor.a = (FinalNormal.y + 1.0) * 0.5; + } +} + +float2 CurrentMipTextureSize; +float2 ParentMipTextureSize; + +void PSMainMips(float2 InTextureCoordinates : TEXCOORD0, + out float4 OutColor : SV_Target0) +{ + bool IsMinBorderTexelX = false; + bool IsMinBorderTexelY = false; + bool IsMaxBorderTexelX = false; + bool IsMaxBorderTexelY = false; + + // Special case of 1 texel size component + if (CurrentMipComponentVertexCount == 1) + { + if (CurrentMipTextureSize.x >= CurrentMipTextureSize.y) // x size 1 texel + { + IsMinBorderTexelY = true; + IsMinBorderTexelX = (InTextureCoordinates.x * CurrentMipTextureSize.x) <= 1.0; + IsMaxBorderTexelX = (InTextureCoordinates.x * CurrentMipTextureSize.x) >= CurrentMipTextureSize.x - 1.0; + } + else + { + IsMinBorderTexelX = true; + IsMinBorderTexelY = (InTextureCoordinates.y * CurrentMipTextureSize.y) <= 1.0; + IsMaxBorderTexelY = (InTextureCoordinates.y * CurrentMipTextureSize.y) >= CurrentMipTextureSize.y - 1.0; + } + } + else + { + float2 CurrentMipQuadTexelSize = 1.0 / CurrentMipComponentVertexCount; + float2 LandscapeQuadUV = frac(InTextureCoordinates * CurrentMipTextureSize / CurrentMipComponentVertexCount); + + IsMinBorderTexelX = LandscapeQuadUV.x <= CurrentMipQuadTexelSize.x; + IsMinBorderTexelY = LandscapeQuadUV.y <= CurrentMipQuadTexelSize.y; + IsMaxBorderTexelX = LandscapeQuadUV.x >= (CurrentMipQuadTexelSize.x * max(CurrentMipComponentVertexCount - 1, 1.0)); + IsMaxBorderTexelY = LandscapeQuadUV.y >= (CurrentMipQuadTexelSize.y * max(CurrentMipComponentVertexCount - 1, 1.0)); + } + + float2 ParentMipTexelSize = 1.0 / ParentMipTextureSize; + float2 SourceUV = InTextureCoordinates - (ParentMipTexelSize * 0.5); + float2 HeightResult = 0; + float2 NormalResult = 0; + OutColor = 0; // Default to 0 + + if (IsMinBorderTexelX) // on left border + { + float4 SourceSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV, 0); + float4 DownSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + float2(0.0, ParentMipTexelSize.y), 0); + float SourceHeight = clamp(float(((int)round(SourceSample.r * 255.0) << 8) | (int)round(SourceSample.g * 255.0)), 0.0, 65536.0); + float DownHeight = clamp(float(((int)round(DownSample.r * 255.0) << 8) | (int)round(DownSample.g * 255.0)), 0.0, 65536.0); + + if (IsMinBorderTexelY) // on top border + { + HeightResult = SourceHeight; + NormalResult = float2(SourceSample.ba); + } + else if (IsMaxBorderTexelY) // on bottom border + { + HeightResult = DownHeight; + NormalResult = float2(DownSample.ba); + } + else + { + float FracTopDown = 0.5; + HeightResult = clamp(round(lerp(SourceHeight, DownHeight, FracTopDown)), 0.0, 65536.0); + NormalResult.x = lerp(SourceSample.b, DownSample.b, FracTopDown); + NormalResult.y = lerp(SourceSample.a, DownSample.a, FracTopDown); + } + } + else if (IsMaxBorderTexelX) // on right border + { + float4 RightSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + float2(ParentMipTexelSize.x, 0.0), 0); + float4 DownRightSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + ParentMipTexelSize, 0); + float RightHeight = clamp(float(((int)round(RightSample.r * 255.0) << 8) | (int)round(RightSample.g * 255.0)), 0.0, 65536.0); + float DownRightHeight = clamp(float(((int)round(DownRightSample.r * 255.0) << 8) | (int)round(DownRightSample.g * 255.0)), 0.0, 65536.0); + + if (IsMinBorderTexelY) // on top border + { + HeightResult = RightHeight; + NormalResult = float2(RightSample.ba); + } + else if (IsMaxBorderTexelY) // on bottom border + { + HeightResult = DownRightHeight; + NormalResult = float2(DownRightSample.ba); + } + else + { + float FracTopDown = 0.5; + HeightResult = clamp(round(lerp(RightHeight, DownRightHeight, FracTopDown)), 0.0, 65536.0);; + NormalResult.x = lerp(RightSample.b, DownRightSample.b, FracTopDown); + NormalResult.y = lerp(RightSample.a, DownRightSample.a, FracTopDown); + } + } + else // center texel + { + float4 SourceSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV, 0); + float4 DownSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + float2(0.0, ParentMipTexelSize.y), 0); + float4 RightSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + float2(ParentMipTexelSize.x, 0.0), 0); + float4 DownRightSample = ReadHeightmapTexture1.SampleLevel(ReadHeightmapTextureSampler, SourceUV + ParentMipTexelSize, 0); + + float SourceHeight = clamp(float(((int)round(SourceSample.r * 255.0) << 8) | (int)round(SourceSample.g * 255.0)), 0.0, 65536.0); + float DownHeight = clamp(float(((int)round(DownSample.r * 255.0) << 8) | (int)round(DownSample.g * 255.0)), 0.0, 65536.0); + float RightHeight = clamp(float(((int)round(RightSample.r * 255.0) << 8) | (int)round(RightSample.g * 255.0)), 0.0, 65536.0); + float DownRightHeight = clamp(float(((int)round(DownRightSample.r * 255.0) << 8) | (int)round(DownRightSample.g * 255.0)), 0.0, 65536.0); + + if (IsMinBorderTexelY) // on top border + { + float FracSourceToRight = 0.5; + HeightResult = clamp(round(lerp(SourceHeight, RightHeight, FracSourceToRight)), 0.0, 65536.0); + NormalResult.x = lerp(SourceSample.b, RightSample.b, FracSourceToRight); + NormalResult.y = lerp(SourceSample.a, RightSample.a, FracSourceToRight); + } + else if (IsMaxBorderTexelY) // on bottom border + { + float FracDownToDownRight = 0.5; + HeightResult = clamp(round(lerp(DownHeight, DownRightHeight, FracDownToDownRight)), 0.0, 65536.0); + NormalResult.x = lerp(DownSample.b, DownRightSample.b, FracDownToDownRight); + NormalResult.y = lerp(DownSample.a, DownRightSample.a, FracDownToDownRight); + } + else + { + float FracSourceToRight = 0.5; + float FracDownToDownRight = 0.5; + + float SourceToRightHeight = lerp(SourceHeight, RightHeight, 0.5); + float DownToDownRightHeight = lerp(DownHeight, DownRightHeight, 0.5); + + float SourceToRightDeltaHeight = RightHeight - 32768.0; + float DownToDownRightDeltaHeight = DownRightHeight - 32768.0; + + float FracTopDown = 0.5; + + HeightResult = clamp(round(lerp(SourceToRightHeight, DownToDownRightHeight, 0.5)), 0.0, 65536.0); + NormalResult.x = lerp(lerp(SourceSample.b, RightSample.b, FracSourceToRight), lerp(DownSample.b, DownRightSample.b, FracDownToDownRight), FracTopDown); + NormalResult.y = lerp(lerp(SourceSample.a, RightSample.a, FracSourceToRight), lerp(DownSample.a, DownRightSample.a, FracDownToDownRight), FracTopDown); + } + } + + float2 PackedHeightResult = float2((float)(((int)HeightResult - ((int)HeightResult & 255)) >> 8) / 255.0, (float)((int)HeightResult & 255) / 255.0); + OutColor = float4(PackedHeightResult.x, PackedHeightResult.y, NormalResult.x, NormalResult.y); +} \ No newline at end of file diff --git a/Engine/Shaders/Private/LandscapeProceduralVS.usf b/Engine/Shaders/Private/LandscapeProceduralVS.usf new file mode 100644 index 000000000000..27641ee26036 --- /dev/null +++ b/Engine/Shaders/Private/LandscapeProceduralVS.usf @@ -0,0 +1,15 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "Common.ush" + +float4x4 Transform; + +void VSMain(in float4 InPosition : ATTRIBUTE0, + in float2 InTextureCoordinate : ATTRIBUTE1, + out float2 OutTextureCoordinate : TEXCOORD0, + out float4 OutPosition : SV_POSITION) +{ + //OutPosition = float4(InPosition.xy, 0, 1); + OutPosition = mul(InPosition, Transform); + OutTextureCoordinate = InTextureCoordinate; +} \ No newline at end of file diff --git a/Engine/Source/Developer/Android/AndroidTargetPlatform/Private/AndroidTargetPlatform.cpp b/Engine/Source/Developer/Android/AndroidTargetPlatform/Private/AndroidTargetPlatform.cpp index 89e50147f1e5..732fcc4dc59c 100644 --- a/Engine/Source/Developer/Android/AndroidTargetPlatform/Private/AndroidTargetPlatform.cpp +++ b/Engine/Source/Developer/Android/AndroidTargetPlatform/Private/AndroidTargetPlatform.cpp @@ -637,6 +637,8 @@ namespace GConfig->GetFloat(CategoryName, TEXT("CompressionQualityModifier"), OutOverrides.CompressionQualityModifier, GEngineIni); + GConfig->GetFloat(CategoryName, TEXT("AutoStreamingThreshold"), OutOverrides.AutoStreamingThreshold, GEngineIni); + //Cache sample rate map. OutOverrides.PlatformSampleRates.Reset(); diff --git a/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.cpp b/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.cpp index 305cd044ba8a..3e091c7c8ee9 100644 --- a/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.cpp +++ b/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.cpp @@ -37,6 +37,9 @@ #include "Factories/PhysicsAssetFactory.h" #include "EditorReimportHandler.h" +#include "Styling/SlateIconFinder.h" +#include "Widgets/Images/SImage.h" +#include "Factories/FbxSkeletalMeshImportData.h" #define LOCTEXT_NAMESPACE "AssetTypeActions" @@ -437,6 +440,14 @@ FDlgMergeSkeleton::EResult FDlgMergeSkeleton::ShowModal() return UserResponse; } +FAssetTypeActions_SkeletalMesh::FAssetTypeActions_SkeletalMesh() +: FAssetTypeActions_Base() +{ + //No need to remove since the asset registry module clear this multicast delegate when terminating + FAssetRegistryModule& AssetRegistryModule = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().OnAssetRemoved().AddRaw(this, &FAssetTypeActions_SkeletalMesh::OnAssetRemoved); +} + void FAssetTypeActions_SkeletalMesh::GetActions( const TArray& InObjects, FMenuBuilder& MenuBuilder ) { auto Meshes = GetTypedWeakObjectPtrs(InObjects); @@ -553,6 +564,72 @@ UThumbnailInfo* FAssetTypeActions_SkeletalMesh::GetThumbnailInfo(UObject* Asset) return ThumbnailInfo; } +EVisibility FAssetTypeActions_SkeletalMesh::GetThumbnailSkinningOverlayVisibility(const FAssetData AssetData) const +{ + //If the asset was delete it will be remove from the list, in that case do not use the + //asset since it can be invalid if the GC has collect the object point by the AssetData. + if (!ThumbnailSkinningOverlayAssetNames.Contains(AssetData.GetFullName())) + { + return EVisibility::Collapsed; + } + + //Prevent loading all assets when we display the thumbnail + //The tags cannot change until the asset is loaded in memory + if (!AssetData.IsAssetLoaded()) + { + FAssetDataTagMapSharedView::FFindTagResult Result = AssetData.TagsAndValues.FindTag(GET_MEMBER_NAME_CHECKED(UFbxSkeletalMeshImportData, LastImportContentType)); + if (Result.IsSet() && Result.GetValue() == TEXT("FBXICT_Geometry")) + { + //Show the icon + return EVisibility::HitTestInvisible; + } + return EVisibility::Collapsed; + } + + //The object is loaded we can use the memory value of the object to set the overlay + UObject* Obj = AssetData.GetAsset(); + USkeletalMesh* SkeletalMesh = CastChecked(Obj); + + UAssetImportData* GenericImportData = SkeletalMesh->AssetImportData; + if (GenericImportData != nullptr) + { + UFbxSkeletalMeshImportData* ImportData = Cast(GenericImportData); + if (ImportData != nullptr && ImportData->LastImportContentType == EFBXImportContentType::FBXICT_Geometry) + { + return EVisibility::HitTestInvisible; + } + } + return EVisibility::Collapsed; +} + +void FAssetTypeActions_SkeletalMesh::OnAssetRemoved(const FAssetData& AssetData) +{ + //Remove the object from the list before it get garbage collect + if (ThumbnailSkinningOverlayAssetNames.Contains(AssetData.GetFullName())) + { + ThumbnailSkinningOverlayAssetNames.Remove(AssetData.GetFullName()); + } +} + +TSharedPtr FAssetTypeActions_SkeletalMesh::GetThumbnailOverlay(const FAssetData& AssetData) const +{ + const FSlateBrush* Icon = FEditorStyle::GetBrush("ClassThumbnailOverlays.SkeletalMesh_NeedSkinning"); + + ThumbnailSkinningOverlayAssetNames.AddUnique(AssetData.GetFullName()); + + return SNew(SBorder) + .BorderImage(FEditorStyle::GetNoBrush()) + .Visibility(this, &FAssetTypeActions_SkeletalMesh::GetThumbnailSkinningOverlayVisibility, AssetData) + .Padding(FMargin(0.0f, 3.0f, 3.0f, 0.0f)) + .HAlign(HAlign_Right) + .VAlign(VAlign_Top) + [ + SNew(SImage) + .ToolTipText(LOCTEXT("FAssetTypeActions_SkeletalMesh_NeedSkinning_ToolTip", "Asset geometry was imported, the skinning need to be validate")) + .Image(Icon) + ]; +} + void FAssetTypeActions_SkeletalMesh::GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFilePaths) const { for (auto& Asset : TypeAssets) @@ -562,6 +639,21 @@ void FAssetTypeActions_SkeletalMesh::GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFileLabels) const +{ + for (auto& Asset : TypeAssets) + { + const auto SkeletalMesh = CastChecked(Asset); + TArray SourceFilePaths; + SkeletalMesh->AssetImportData->ExtractFilenames(SourceFilePaths); + for (int32 SourceIndex = 0; SourceIndex < SourceFilePaths.Num(); ++SourceIndex) + { + FText SourceIndexLabel = SourceIndex == 0 ? NSSkeletalMeshSourceFileLabels::GeoAndSkinningText() : SourceIndex == 1 ? NSSkeletalMeshSourceFileLabels::GeometryText() : NSSkeletalMeshSourceFileLabels::SkinningText(); + OutSourceFileLabels.Add(SourceIndexLabel.ToString()); + } + } +} + void FAssetTypeActions_SkeletalMesh::GetLODMenu(class FMenuBuilder& MenuBuilder,TArray> Objects) { check(Objects.Num() > 0); diff --git a/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.h b/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.h index 73325c52d6cd..98c7f2071d53 100644 --- a/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.h +++ b/Engine/Source/Developer/AssetTools/Private/AssetTypeActions/AssetTypeActions_SkeletalMesh.h @@ -12,6 +12,8 @@ class FMenuBuilder; class FAssetTypeActions_SkeletalMesh : public FAssetTypeActions_Base { public: + FAssetTypeActions_SkeletalMesh(); + // IAssetTypeActions Implementation virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_SkeletalMesh", "Skeletal Mesh"); } virtual FColor GetTypeColor() const override { return FColor(255,0,255); } @@ -23,8 +25,13 @@ public: virtual class UThumbnailInfo* GetThumbnailInfo(UObject* Asset) const override; virtual bool IsImportedAsset() const override { return true; } virtual void GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFilePaths) const override; + virtual void GetSourceFileLabels(const TArray& TypeAssets, TArray& OutSourceFileLabels) const override; + virtual TSharedPtr GetThumbnailOverlay(const FAssetData& AssetData) const override; private: + /* If the skeletal mesh asset was lastly import with geometry only, we want to add an overlay icon to tell users.*/ + EVisibility GetThumbnailSkinningOverlayVisibility(const FAssetData AssetData) const; + /** Handler for when skeletal mesh LOD import is selected */ void LODImport(TArray> Objects); @@ -62,4 +69,8 @@ private: void FillSourceMenu(FMenuBuilder& MenuBuilder, TArray> Meshes) const; void FillSkeletonMenu(FMenuBuilder& MenuBuilder, TArray> Meshes) const; void FillCreateMenu(FMenuBuilder& MenuBuilder, TArray> Meshes) const; + + void OnAssetRemoved(const struct FAssetData& AssetData); + + mutable TArray ThumbnailSkinningOverlayAssetNames; }; diff --git a/Engine/Source/Developer/AssetTools/Public/AssetTypeActions_Base.h b/Engine/Source/Developer/AssetTools/Public/AssetTypeActions_Base.h index 6a17de6391f1..edd48873a4ae 100644 --- a/Engine/Source/Developer/AssetTools/Public/AssetTypeActions_Base.h +++ b/Engine/Source/Developer/AssetTools/Public/AssetTypeActions_Base.h @@ -125,6 +125,17 @@ public: { } + virtual void GetSourceFileLabels(const TArray& TypeAssets, TArray& OutSourceFileLabels) const override + { + TArray SourceFilePaths; + OutSourceFileLabels.Reset(); + GetResolvedSourceFilePaths(TypeAssets, SourceFilePaths); + if (SourceFilePaths.Num() > 0) + { + OutSourceFileLabels.AddDefaulted(SourceFilePaths.Num()); + } + } + virtual void BuildBackendFilter(FARFilter& InFilter) override { // Add the supported class for this type to a filter diff --git a/Engine/Source/Developer/AssetTools/Public/IAssetTypeActions.h b/Engine/Source/Developer/AssetTools/Public/IAssetTypeActions.h index 49abf8d0796c..0e4d7495a612 100644 --- a/Engine/Source/Developer/AssetTools/Public/IAssetTypeActions.h +++ b/Engine/Source/Developer/AssetTools/Public/IAssetTypeActions.h @@ -104,6 +104,9 @@ public: /** Collects the resolved source paths for the imported assets */ virtual void GetResolvedSourceFilePaths(const TArray& TypeAssets, TArray& OutSourceFilePaths) const = 0; + + /** Collects the source file labels for the imported assets */ + virtual void GetSourceFileLabels(const TArray& TypeAssets, TArray& OutSourceFileLabels) const = 0; /** Builds the filter for this class*/ virtual void BuildBackendFilter(struct FARFilter& InFilter) = 0; diff --git a/Engine/Source/Developer/BlueprintCompilerCppBackend/Private/BlueprintCompilerCppBackendUtils.cpp b/Engine/Source/Developer/BlueprintCompilerCppBackend/Private/BlueprintCompilerCppBackendUtils.cpp index ca732c258120..a4afa58d8fe0 100644 --- a/Engine/Source/Developer/BlueprintCompilerCppBackend/Private/BlueprintCompilerCppBackendUtils.cpp +++ b/Engine/Source/Developer/BlueprintCompilerCppBackend/Private/BlueprintCompilerCppBackendUtils.cpp @@ -1215,6 +1215,11 @@ FString FEmitHelper::LiteralTerm(FEmitterLocalContext& EmitterContext, const FLi int32 Value = CustomValue.IsEmpty() ? 0 : FCString::Atoi(*CustomValue); return FString::Printf(TEXT("%d"), Value); } + else if (UEdGraphSchema_K2::PC_Int64 == Type.PinCategory) + { + int64 Value = CustomValue.IsEmpty() ? 0 : FCString::Atoi64(*CustomValue); + return FString::Printf(TEXT("%lld"), Value); + } else if ((UEdGraphSchema_K2::PC_Byte == Type.PinCategory) || (UEdGraphSchema_K2::PC_Enum == Type.PinCategory)) { UEnum* TypeEnum = Cast(Type.PinSubCategoryObject.Get()); @@ -1444,6 +1449,10 @@ FString FEmitHelper::PinTypeToNativeType(const FEdGraphPinType& Type) { return TEXT("int32"); } + else if (UEdGraphSchema_K2::PC_Int64 == InType.PinCategory) + { + return TEXT("int64"); + } else if (UEdGraphSchema_K2::PC_Float == InType.PinCategory) { return TEXT("float"); diff --git a/Engine/Source/Developer/CrashDebugHelper/Private/Windows/WindowsPlatformStackWalkExt.cpp b/Engine/Source/Developer/CrashDebugHelper/Private/Windows/WindowsPlatformStackWalkExt.cpp index 0363654e8822..2963a87679ae 100644 --- a/Engine/Source/Developer/CrashDebugHelper/Private/Windows/WindowsPlatformStackWalkExt.cpp +++ b/Engine/Source/Developer/CrashDebugHelper/Private/Windows/WindowsPlatformStackWalkExt.cpp @@ -525,7 +525,7 @@ int FWindowsPlatformStackWalkExt::GetCallstacks(bool bTrimCallstack) if( FunctionName.Len() > 0 ) { if( FunctionName.Contains( TEXT( "FDebug::" ), ESearchCase::CaseSensitive ) - || FunctionName.Contains( TEXT( "NewReportEnsure" ), ESearchCase::CaseSensitive ) ) + || FunctionName.Contains( TEXT( "ReportEnsure" ), ESearchCase::CaseSensitive ) ) { bFoundSourceFile = false; AssertOrEnsureIndex = Exception.CallStackString.Num(); diff --git a/Engine/Source/Developer/IOS/IOSPlatformEditor/Private/IOSTargetSettingsCustomization.cpp b/Engine/Source/Developer/IOS/IOSPlatformEditor/Private/IOSTargetSettingsCustomization.cpp index b184060835af..c8c3447154a7 100644 --- a/Engine/Source/Developer/IOS/IOSPlatformEditor/Private/IOSTargetSettingsCustomization.cpp +++ b/Engine/Source/Developer/IOS/IOSPlatformEditor/Private/IOSTargetSettingsCustomization.cpp @@ -286,6 +286,7 @@ void FIOSTargetSettingsCustomization::BuildPListSection(IDetailLayoutBuilder& De IDetailCategoryBuilder& AppManifestCategory = DetailLayout.EditCategory(TEXT("Info.plist")); IDetailCategoryBuilder& BundleCategory = DetailLayout.EditCategory(TEXT("BundleInformation")); IDetailCategoryBuilder& OrientationCategory = DetailLayout.EditCategory(TEXT("Orientation")); + IDetailCategoryBuilder& FileSystemCategory = DetailLayout.EditCategory(TEXT("FileSystem")); IDetailCategoryBuilder& RenderCategory = DetailLayout.EditCategory(TEXT("Rendering")); IDetailCategoryBuilder& OSInfoCategory = DetailLayout.EditCategory(TEXT("OS Info")); IDetailCategoryBuilder& DeviceCategory = DetailLayout.EditCategory(TEXT("Devices")); @@ -808,6 +809,8 @@ void FIOSTargetSettingsCustomization::BuildPListSection(IDetailLayoutBuilder& De SETUP_PLIST_PROP(bSupportsLandscapeRightOrientation, OrientationCategory); SETUP_PLIST_PROP(PreferredLandscapeOrientation, OrientationCategory); + SETUP_PLIST_PROP(bSupportsITunesFileSharing, FileSystemCategory); + SETUP_PLIST_PROP(bSupportsMetal, RenderCategory); MRTPropertyHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UIOSRuntimeSettings, bSupportsMetalMRT)); diff --git a/Engine/Source/Developer/IOS/IOSTargetPlatform/Private/IOSTargetPlatform.cpp b/Engine/Source/Developer/IOS/IOSTargetPlatform/Private/IOSTargetPlatform.cpp index 869580464dc3..f96178028d0f 100644 --- a/Engine/Source/Developer/IOS/IOSTargetPlatform/Private/IOSTargetPlatform.cpp +++ b/Engine/Source/Developer/IOS/IOSTargetPlatform/Private/IOSTargetPlatform.cpp @@ -719,6 +719,8 @@ namespace GConfig->GetFloat(CategoryName, TEXT("CompressionQualityModifier"), OutOverrides.CompressionQualityModifier, GEngineIni); + GConfig->GetFloat(CategoryName, TEXT("AutoStreamingThreshold"), OutOverrides.AutoStreamingThreshold, GEngineIni); + //Cache sample rate map. OutOverrides.PlatformSampleRates.Reset(); diff --git a/Engine/Source/Developer/Localization/Private/TextLocalizationResourceGenerator.cpp b/Engine/Source/Developer/Localization/Private/TextLocalizationResourceGenerator.cpp index 824801d9efb9..fbad06c8e3e1 100644 --- a/Engine/Source/Developer/Localization/Private/TextLocalizationResourceGenerator.cpp +++ b/Engine/Source/Developer/Localization/Private/TextLocalizationResourceGenerator.cpp @@ -18,10 +18,13 @@ bool FTextLocalizationResourceGenerator::GenerateLocMeta(const FLocTextHelper& I return true; } -bool FTextLocalizationResourceGenerator::GenerateLocRes(const FLocTextHelper& InLocTextHelper, const FString& InCultureToGenerate, const bool bSkipSourceCheck, const FTextLocalizationResourceId& InLocResID, FTextLocalizationResource& OutLocRes) +bool FTextLocalizationResourceGenerator::GenerateLocRes(const FLocTextHelper& InLocTextHelper, const FString& InCultureToGenerate, const bool bSkipSourceCheck, const FTextKey& InLocResID, FTextLocalizationResource& OutLocRes, const int32 InPriority) { + const bool bIsNativeCulture = InCultureToGenerate == InLocTextHelper.GetNativeCulture(); + FCulturePtr Culture = FInternationalization::Get().GetCulture(InCultureToGenerate); + // Add each manifest entry to the LocRes file - InLocTextHelper.EnumerateSourceTexts([&InLocTextHelper, &InCultureToGenerate, &bSkipSourceCheck, &InLocResID, &OutLocRes](TSharedRef InManifestEntry) -> bool + InLocTextHelper.EnumerateSourceTexts([&InLocTextHelper, &InCultureToGenerate, &bSkipSourceCheck, &InLocResID, &OutLocRes, InPriority, bIsNativeCulture, Culture](TSharedRef InManifestEntry) -> bool { // For each context, we may need to create a different or even multiple LocRes entries. for (const FManifestContext& Context : InManifestEntry->Contexts) @@ -30,15 +33,35 @@ bool FTextLocalizationResourceGenerator::GenerateLocRes(const FLocTextHelper& In FLocItem TranslationText; InLocTextHelper.GetRuntimeText(InCultureToGenerate, InManifestEntry->Namespace, Context.Key, Context.KeyMetadataObj, ELocTextExportSourceMethod::NativeText, InManifestEntry->Source, TranslationText, bSkipSourceCheck); - // Add this entry to the LocRes - OutLocRes.AddEntry(InManifestEntry->Namespace.GetString(), Context.Key.GetString(), InManifestEntry->Source.Text, TranslationText.Text, InLocResID); + // Is this entry considered translated? Native entries are always translated + const bool bIsTranslated = bIsNativeCulture || !InManifestEntry->Source.IsExactMatch(TranslationText); + if (bIsTranslated) + { + // Validate translations that look like they could be format patterns + if (Culture && TranslationText.Text.Contains(TEXT("{"), ESearchCase::CaseSensitive)) + { + const FTextFormat FmtPattern = FTextFormat::FromString(TranslationText.Text); + + TArray ValidationErrors; + if (!FmtPattern.ValidatePattern(Culture, ValidationErrors)) + { + FString Message = FString::Printf(TEXT("Format pattern '%s' (%s,%s) generated the following validation errors for '%s':"), *TranslationText.Text, *InManifestEntry->Namespace.GetString(), *Context.Key.GetString(), *InCultureToGenerate); + for (const FString& ValidationError : ValidationErrors) + { + Message += FString::Printf(TEXT("\n - %s"), *ValidationError); + } + UE_LOG(LogTextLocalizationResourceGenerator, Warning, TEXT("%s"), *FLocTextHelper::SanitizeLogOutput(Message)); + } + } + + // Add this entry to the LocRes + OutLocRes.AddEntry(InManifestEntry->Namespace.GetString(), Context.Key.GetString(), InManifestEntry->Source.Text, TranslationText.Text, InPriority, InLocResID); + } } return true; // continue enumeration }, true); - OutLocRes.DetectAndLogConflicts(); - return true; } @@ -142,20 +165,21 @@ bool FTextLocalizationResourceGenerator::GenerateLocResAndUpdateLiveEntriesFromC } } - TArray> TextLocalizationResources; - for (const FString& CultureName : CulturesToGenerate) + FTextLocalizationResource TextLocalizationResource; + for (int32 CultureIndex = 0; CultureIndex < CulturesToGenerate.Num(); ++CultureIndex) { + const FString& CultureName = CulturesToGenerate[CultureIndex]; + const FString CulturePath = DestinationPath / CultureName; const FString ResourceFilePath = FPaths::ConvertRelativePathToFull(CulturePath / ResourceName); - TSharedPtr& LocRes = TextLocalizationResources.Add_GetRef(MakeShared()); - if (!GenerateLocRes(LocTextHelper, CultureName, bSkipSourceCheck, FTextLocalizationResourceId(ResourceFilePath), *LocRes)) + if (!GenerateLocRes(LocTextHelper, CultureName, bSkipSourceCheck, FTextKey(ResourceFilePath), TextLocalizationResource, CultureIndex)) { UE_LOG(LogTextLocalizationResourceGenerator, Error, TEXT("Failed to generate localization resource for culture '%s'."), *CultureName); return false; } } - FTextLocalizationManager::Get().UpdateFromLocalizationResources(TextLocalizationResources); + FTextLocalizationManager::Get().UpdateFromLocalizationResource(TextLocalizationResource); return true; } diff --git a/Engine/Source/Developer/Localization/Public/TextLocalizationResourceGenerator.h b/Engine/Source/Developer/Localization/Public/TextLocalizationResourceGenerator.h index 01c90df33865..1664e329edbd 100644 --- a/Engine/Source/Developer/Localization/Public/TextLocalizationResourceGenerator.h +++ b/Engine/Source/Developer/Localization/Public/TextLocalizationResourceGenerator.h @@ -3,7 +3,7 @@ #pragma once #include "CoreMinimal.h" -#include "Internationalization/TextLocalizationResourceId.h" +#include "Internationalization/TextKey.h" class FLocTextHelper; class FTextLocalizationMetaDataResource; @@ -21,7 +21,7 @@ public: /** * Given a loc text helper, generate a compiled LocRes resource for the given culture. */ - LOCALIZATION_API static bool GenerateLocRes(const FLocTextHelper& InLocTextHelper, const FString& InCultureToGenerate, const bool bSkipSourceCheck, const FTextLocalizationResourceId& InLocResID, FTextLocalizationResource& OutLocRes); + LOCALIZATION_API static bool GenerateLocRes(const FLocTextHelper& InLocTextHelper, const FString& InCultureToGenerate, const bool bSkipSourceCheck, const FTextKey& InLocResID, FTextLocalizationResource& OutLocRes, const int32 InPriority = 0); /** * Given a config file, generate a compiled LocRes resource for the active culture and use it to update the live-entries in the localization manager. diff --git a/Engine/Source/Developer/MeshReductionInterface/Public/IMeshReductionInterfaces.h b/Engine/Source/Developer/MeshReductionInterface/Public/IMeshReductionInterfaces.h index 95d1c3715d5c..206a8c26bbcb 100644 --- a/Engine/Source/Developer/MeshReductionInterface/Public/IMeshReductionInterfaces.h +++ b/Engine/Source/Developer/MeshReductionInterface/Public/IMeshReductionInterfaces.h @@ -51,6 +51,12 @@ public: * Returns true if mesh reduction is supported */ virtual bool IsSupported() const = 0; + + /** + * Returns true if mesh reduction is active. Active mean there will be a reduction of the vertices or triangle number + */ + virtual bool IsReductionActive(const struct FMeshReductionSettings &ReductionSettings) const = 0; + virtual bool IsReductionActive(const struct FSkeletalMeshOptimizationSettings &ReductionSettings) const = 0; }; DECLARE_DELEGATE_ThreeParams(FProxyCompleteDelegate, struct FMeshDescription&, struct FFlattenMaterial&, const FGuid); diff --git a/Engine/Source/Developer/MeshSimplifier/Private/QuadricMeshReduction.cpp b/Engine/Source/Developer/MeshSimplifier/Private/QuadricMeshReduction.cpp index b7fbf71b1980..e57581995e58 100644 --- a/Engine/Source/Developer/MeshSimplifier/Private/QuadricMeshReduction.cpp +++ b/Engine/Source/Developer/MeshSimplifier/Private/QuadricMeshReduction.cpp @@ -586,6 +586,38 @@ public: return true; } + /** + * Returns true if mesh reduction is active. Active mean there will be a reduction of the vertices or triangle number + */ + virtual bool IsReductionActive(const struct FMeshReductionSettings &ReductionSettings) const + { + float Threshold_One = (1.0f - KINDA_SMALL_NUMBER); + switch (ReductionSettings.TerminationCriterion) + { + case EStaticMeshReductionTerimationCriterion::Triangles: + { + return ReductionSettings.PercentTriangles < Threshold_One; + } + break; + case EStaticMeshReductionTerimationCriterion::Vertices: + { + return ReductionSettings.PercentVertices < Threshold_One; + } + break; + case EStaticMeshReductionTerimationCriterion::Any: + { + return ReductionSettings.PercentTriangles < Threshold_One || ReductionSettings.PercentVertices < Threshold_One; + } + break; + } + return false; + } + + virtual bool IsReductionActive(const FSkeletalMeshOptimizationSettings &ReductionSettings) const + { + return false; + } + virtual ~FQuadricSimplifierMeshReduction() {} static FQuadricSimplifierMeshReduction* Create() diff --git a/Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cpp b/Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cpp index c6f02ed101b0..7843bf8e9dc1 100644 --- a/Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cpp +++ b/Engine/Source/Developer/MeshUtilities/Private/MeshUtilities.cpp @@ -4406,8 +4406,8 @@ void TangentFailSafe(const FVector &TriangleTangentX, const FVector &TriangleTan else { //TangentX and Y are invalid, use the triangle data, can cause a hard edge - TangentX = TriangleTangentX; - TangentY = TriangleTangentY; + TangentX = TriangleTangentX.GetSafeNormal(); + TangentY = TriangleTangentY.GetSafeNormal(); } } else if (!bTangentXZero) @@ -4420,43 +4420,68 @@ void TangentFailSafe(const FVector &TriangleTangentX, const FVector &TriangleTan else { //TangentY and Z are invalid, use the triangle data, can cause a hard edge - TangentZ = TriangleTangentZ; - TangentY = TriangleTangentY; + TangentZ = TriangleTangentZ.GetSafeNormal(); + TangentY = TriangleTangentY.GetSafeNormal(); } } else if (!bTangentYZero) { //TangentX and Z are invalid, use the triangle data, can cause a hard edge - TangentX = TriangleTangentX; - TangentZ = TriangleTangentZ; + TangentX = TriangleTangentX.GetSafeNormal(); + TangentZ = TriangleTangentZ.GetSafeNormal(); } else { //Everything is zero, use all triangle data, can cause a hard edge - TangentX = TriangleTangentX; - TangentY = TriangleTangentY; - TangentZ = TriangleTangentZ; + TangentX = TriangleTangentX.GetSafeNormal(); + TangentY = TriangleTangentY.GetSafeNormal(); + TangentZ = TriangleTangentZ.GetSafeNormal(); } - //Ortho normalize the result - TangentY -= TangentX * (TangentX | TangentY); - TangentY.Normalize(); - - TangentX -= TangentZ * (TangentZ | TangentX); - TangentY -= TangentZ * (TangentZ | TangentY); - - TangentX.Normalize(); - TangentY.Normalize(); - - //If we still have some zero data (i.e. triangle data is degenerated) - if (TangentZ.IsNearlyZero() || TangentZ.ContainsNaN() - || TangentX.IsNearlyZero() || TangentX.ContainsNaN() - || TangentY.IsNearlyZero() || TangentY.ContainsNaN()) + bool bParaXY = FVector::Parallel(TangentX, TangentY); + bool bParaYZ = FVector::Parallel(TangentY, TangentZ); + bool bParaZX = FVector::Parallel(TangentZ, TangentX); + if (bParaXY || bParaYZ || bParaZX) { - //Since the triangle is degenerate this case can cause a hardedge, but will probably have no other impact since the triangle is degenerate (no visible surface) - TangentX = FVector(1.0f, 0.0f, 0.0f); - TangentY = FVector(0.0f, 1.0f, 0.0f); - TangentZ = FVector(0.0f, 0.0f, 1.0f); + //In case XY are parallel, use the Z(normal) if valid and not parallel to both X and Y to find the missing component + if (bParaXY && !bParaZX) + { + TangentY = FVector::CrossProduct(TangentZ, TangentX).GetSafeNormal(); + } + else if (bParaXY && !bParaYZ) + { + TangentX = FVector::CrossProduct(TangentY, TangentZ).GetSafeNormal(); + } + else + { + //Degenerated value put something valid + TangentX = FVector(1.0f, 0.0f, 0.0f); + TangentY = FVector(0.0f, 1.0f, 0.0f); + TangentZ = FVector(0.0f, 0.0f, 1.0f); + } + } + else + { + //Ortho normalize the result + TangentY -= TangentX * (TangentX | TangentY); + TangentY.Normalize(); + + TangentX -= TangentZ * (TangentZ | TangentX); + TangentY -= TangentZ * (TangentZ | TangentY); + + TangentX.Normalize(); + TangentY.Normalize(); + + //If we still have some zero data (i.e. triangle data is degenerated) + if (TangentZ.IsNearlyZero() || TangentZ.ContainsNaN() + || TangentX.IsNearlyZero() || TangentX.ContainsNaN() + || TangentY.IsNearlyZero() || TangentY.ContainsNaN()) + { + //Since the triangle is degenerate this case can cause a hardedge, but will probably have no other impact since the triangle is degenerate (no visible surface) + TangentX = FVector(1.0f, 0.0f, 0.0f); + TangentY = FVector(0.0f, 1.0f, 0.0f); + TangentZ = FVector(0.0f, 0.0f, 1.0f); + } } } diff --git a/Engine/Source/Developer/PakFileUtilities/Private/PakFileUtilities.cpp b/Engine/Source/Developer/PakFileUtilities/Private/PakFileUtilities.cpp index 254f3f260dd4..7ba47093c464 100644 --- a/Engine/Source/Developer/PakFileUtilities/Private/PakFileUtilities.cpp +++ b/Engine/Source/Developer/PakFileUtilities/Private/PakFileUtilities.cpp @@ -42,6 +42,8 @@ struct FPakCommandLineParameters , FileSystemBlockSize(0) , PatchFilePadAlign(0) , GeneratePatch(false) + , PatchSeekOptMaxGapSize(0) + , PatchSeekOptUseOrder(false) , EncryptIndex(false) , UseCustomCompressor(false) , OverridePlatformCompressor(false) @@ -54,6 +56,8 @@ struct FPakCommandLineParameters bool GeneratePatch; FString SourcePatchPakFilename; FString SourcePatchDiffDirectory; + int64 PatchSeekOptMaxGapSize; + bool PatchSeekOptUseOrder; bool EncryptIndex; bool UseCustomCompressor; bool OverridePlatformCompressor; @@ -146,6 +150,31 @@ struct FCompressedFileBuffer TUniquePtr CompressedBuffer; }; +template +bool ReadSizeParam(const TCHAR* CmdLine, const TCHAR* ParamStr, T& SizeOut) +{ + FString ParamValueStr; + if (FParse::Value(CmdLine, ParamStr, ParamValueStr) && + FParse::Value(CmdLine, ParamStr, SizeOut)) + { + if (ParamValueStr.EndsWith(TEXT("GB"))) + { + SizeOut *= 1024 * 1024 * 1024; + } + else if (ParamValueStr.EndsWith(TEXT("MB"))) + { + SizeOut *= 1024 * 1024; + } + else if (ParamValueStr.EndsWith(TEXT("KB"))) + { + SizeOut *= 1024; + } + return true; + } + return false; +} + + FString GetLongestPath(TArray& FilesToAdd) { FString LongestPath; @@ -382,7 +411,7 @@ void PrepareDeleteRecordForPak(const FString& InMountPoint, const FPakInputPair OutNewEntry.Info.SetDeleteRecord(true); } -bool ProcessOrderFile(const TCHAR* ResponseFile, TMap& OrderMap) +bool ProcessOrderFile(const TCHAR* ResponseFile, TMap& OrderMap, bool bSecondaryOrderFile = false, int32 OrderOffset = 0) { // List of all items to add to pak file FString Text; @@ -421,7 +450,11 @@ bool ProcessOrderFile(const TCHAR* ResponseFile, TMap& OrderMap OpenOrderNumber += (1 << 30); } #endif - OrderMap.Add(Path, OpenOrderNumber); + if (bSecondaryOrderFile && OrderMap.Contains(Path) ) + { + continue; + } + OrderMap.Add(Path, OpenOrderNumber + OrderOffset); } UE_LOG(LogPakFile, Display, TEXT("Finished loading pak order file %s."), ResponseFile); return true; @@ -482,7 +515,12 @@ void PreProcessCommandline(const TCHAR* CmdLine, FPakCommandLineParameters& CmdL return; } - IModularFeatures::Get().RegisterModularFeature(CUSTOM_COMPRESSOR_FEATURE_NAME, Compressor); + static FCriticalSection CritSec; + + { + FScopeLock Lock(&CritSec); + IModularFeatures::Get().RegisterModularFeature(CUSTOM_COMPRESSOR_FEATURE_NAME, Compressor); + } CmdLineParameters.UseCustomCompressor = true; } @@ -492,38 +530,11 @@ void ProcessCommandLine(const TCHAR* CmdLine, const TArray& NonOptionAr { // List of all items to add to pak file FString ResponseFile; - FString ClusterSizeString; - - if (FParse::Value(CmdLine, TEXT("-blocksize="), ClusterSizeString) && - FParse::Value(CmdLine, TEXT("-blocksize="), CmdLineParameters.FileSystemBlockSize)) - { - if (ClusterSizeString.EndsWith(TEXT("MB"))) - { - CmdLineParameters.FileSystemBlockSize *= 1024*1024; - } - else if (ClusterSizeString.EndsWith(TEXT("KB"))) - { - CmdLineParameters.FileSystemBlockSize *= 1024; - } - } - else + if (!ReadSizeParam(CmdLine, TEXT("-blocksize="), CmdLineParameters.FileSystemBlockSize)) { CmdLineParameters.FileSystemBlockSize = 0; } - - FString CompBlockSizeString; - if (FParse::Value(CmdLine, TEXT("-compressionblocksize="), CompBlockSizeString) && - FParse::Value(CmdLine, TEXT("-compressionblocksize="), CmdLineParameters.CompressionBlockSize)) - { - if (CompBlockSizeString.EndsWith(TEXT("MB"))) - { - CmdLineParameters.CompressionBlockSize *= 1024 * 1024; - } - else if (CompBlockSizeString.EndsWith(TEXT("KB"))) - { - CmdLineParameters.CompressionBlockSize *= 1024; - } - } + ReadSizeParam(CmdLine, TEXT("-compressionblocksize="), CmdLineParameters.CompressionBlockSize); if (!FParse::Value(CmdLine, TEXT("-bitwindow="), CmdLineParameters.CompressionBitWindow)) { @@ -540,6 +551,12 @@ void ProcessCommandLine(const TCHAR* CmdLine, const TArray& NonOptionAr CmdLineParameters.EncryptIndex = true; } + FString EncryptionKeyGuid; + if (FParse::Value(CmdLine, TEXT("EncryptionKeyOverrideGuid="), EncryptionKeyGuid)) + { + FGuid::Parse(EncryptionKeyGuid, CmdLineParameters.EncryptionKeyGuid); + } + if (FParse::Param(CmdLine, TEXT("overrideplatformcompressor"))) { CmdLineParameters.OverridePlatformCompressor = true; @@ -551,6 +568,12 @@ void ProcessCommandLine(const TCHAR* CmdLine, const TArray& NonOptionAr CmdLineParameters.GeneratePatch = FParse::Value(CmdLine, TEXT("-generatepatch="), CmdLineParameters.SourcePatchPakFilename); + if (CmdLineParameters.GeneratePatch) + { + ReadSizeParam(CmdLine, TEXT("-patchSeekOptMaxGapSize="), CmdLineParameters.PatchSeekOptMaxGapSize); + CmdLineParameters.PatchSeekOptUseOrder = FParse::Param(CmdLine, TEXT("patchSeekOptUseOrder")); + } + bool bCompress = FParse::Param(CmdLine, TEXT("compress")); bool bEncrypt = FParse::Param(CmdLine, TEXT("encrypt")); @@ -661,6 +684,59 @@ void ProcessCommandLine(const TCHAR* CmdLine, const TArray& NonOptionAr UE_LOG(LogPakFile, Display, TEXT("Added %d entries to add to pak file."), Entries.Num()); } +FString RemapLocalizationPathIfNeeded(const FString& PathLower, FString& OutRegion) +{ + static const TCHAR* L10NPrefix = (const TCHAR*)TEXT("/content/l10n/"); + static const int32 L10NPrefixLength = FCString::Strlen(L10NPrefix); + int32 FoundIndex = PathLower.Find(L10NPrefix, ESearchCase::CaseSensitive); + if (FoundIndex > 0) + { + // Validate the content index is the first one + int32 ContentIndex = PathLower.Find(TEXT("/content/"), ESearchCase::CaseSensitive); + if (ContentIndex == FoundIndex) + { + int32 EndL10NOffset = ContentIndex + L10NPrefixLength; + int32 NextSlashIndex = PathLower.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, EndL10NOffset); + int32 RegionLength = NextSlashIndex - EndL10NOffset; + if (RegionLength >= 2) + { + FString NonLocalizedPath = PathLower.Mid(0, ContentIndex) + TEXT("/content") + PathLower.Mid(NextSlashIndex); + OutRegion = PathLower.Mid(EndL10NOffset, RegionLength); + return NonLocalizedPath; + } + } + } + return PathLower; +} + +uint64 GetFileOrder(const FString Path, const TMap& OrderMap) +{ + FString RegionStr; + FString NewPath = RemapLocalizationPathIfNeeded(Path.ToLower(), RegionStr); + const uint64* FoundOrder = OrderMap.Find(NewPath); + if (FoundOrder == nullptr) + { + return MAX_uint64; + } + + // Optionally offset based on region, so multiple files in different regions don't get the same order. + // I/O profiling suggests this is slightly worse, so leaving this disabled for now +#if 0 + if (RegionStr.Len() > 0) + { + uint64 RegionOffset = 0; + for (int i = 0; i < RegionStr.Len(); i++) + { + int8 Letter = (int8)(RegionStr[i] - TEXT('a')); + RegionOffset |= (uint64(Letter) << (i*5)); + } + return *FoundOrder + (RegionOffset << 16); + } +#endif + return *FoundOrder; +} + + void CollectFilesToAdd(TArray& OutFilesToAdd, const TArray& InEntries, const TMap& OrderMap) { UE_LOG(LogPakFile, Display, TEXT("Collecting files to add to pak file...")); @@ -697,10 +773,11 @@ void CollectFilesToAdd(TArray& OutFilesToAdd, const TArray& OutFilesToAdd, const TArray& OutFilesToAdd, const TArray Lines; Lines.Empty(Records.Num()+2); - Lines.Add(TEXT("Filename, Offset, Size, Hash, Deleted")); + Lines.Add(TEXT("Filename, Offset, Size, Hash, Deleted, Compressed, CompressionMethod")); for (auto It : Records) { const FPakEntry& Entry = It.Info(); + bool bWasCompressed = Entry.CompressionMethod != COMPRESS_None; + Lines.Add( FString::Printf( - TEXT("%s%s, %d, %d, %s, %s"), + TEXT("%s%s, %d, %d, %s, %s, %s, %d"), *MountPoint, *It.Filename(), Entry.Offset, Entry.Size, *BytesToHex(Entry.Hash, sizeof(Entry.Hash)), - Entry.IsDeleteRecord() ? TEXT("true") : TEXT("false")) ); + Entry.IsDeleteRecord() ? TEXT("true") : TEXT("false"), + bWasCompressed ? TEXT("true") : TEXT("false"), + Entry.CompressionMethod) ); } if (FFileHelper::SaveStringArrayToFile(Lines, *CSVFilename) == false) @@ -1836,7 +1917,7 @@ int32 GetPakChunkIndexFromFilename( const FString& PakFilePath ) return PakChunkIndex; } -bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& CSVFilename, bool bSigned ) +bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& CSVFilename, bool bSigned, const TMap& OrderMap, bool bSortByOrdering ) { //collect all pak files FString PakFileDirectory; @@ -1867,6 +1948,7 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& }; TMap FileRevisions; TMap DeletedRevisions; + TMap PakFilenameToPatchDotChunk; int32 HighestPakPriority = -1; //build lookup tables for the newest revision of all files and all deleted files @@ -1907,6 +1989,17 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& AppropriateRevisions[AssetName] = Revision; } } + + + //build "patch.chunk" string + FString PatchDotChunk; + PatchDotChunk += FString::Printf( TEXT("%d."), PakPriority+1 ); + int32 ChunkIndex = GetPakChunkIndexFromFilename( PakFilename ); + if( ChunkIndex != -1 ) + { + PatchDotChunk += FString::Printf( TEXT("%d"), ChunkIndex ); + } + PakFilenameToPatchDotChunk.Add( PakFileList[PakFileIndex], PatchDotChunk ); } else { @@ -1915,6 +2008,8 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& } } + bool bHasOpenOrder = (OrderMap.Num() > 0); + //open CSV file, if requested FArchive* CSVFileWriter = nullptr; if( !CSVFilename.IsEmpty() ) @@ -1940,22 +2035,90 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& } }; - //log every file, sorted alphabetically - FileRevisions.KeySort([]( const FString& A, const FString& B ) + //cache open order for faster lookup + TMap CachedOpenOrder; + if( bHasOpenOrder ) { - return A.Compare(B, ESearchCase::IgnoreCase) < 0; - }); - DeletedRevisions.KeySort([]( const FString& A, const FString& B ) - { - return A.Compare(B, ESearchCase::IgnoreCase) < 0; - }); + UE_LOG(LogPakFile, Display, TEXT("Checking open order data") ); + for (auto Itr : FileRevisions) + { + const FString& AssetPath = Itr.Key; + FString OpenOrderAssetName = FString::Printf( TEXT("../../../%s"), *AssetPath ); + FPaths::NormalizeFilename(OpenOrderAssetName); + OpenOrderAssetName.ToLowerInline(); - WriteCSVLine( TEXT("AssetName,State,Pak,Prev.Pak,Rev,Prev.Rev,Size,AssetPath") ); + if( const uint64* OrderIndexPtr = OrderMap.Find( OpenOrderAssetName ) ) + { + CachedOpenOrder.Add( AssetPath, *OrderIndexPtr ); + } + } + } + + //helper lambda to look up cached open order + auto FindOpenOrder = [&]( const FString& AssetPath ) + { + if( const uint64* OrderIndexPtr = CachedOpenOrder.Find( AssetPath ) ) + { + return (*OrderIndexPtr); + } + + return uint64(UINT64_MAX); + }; + + //log every file, sorted alphabetically + if( bSortByOrdering && bHasOpenOrder ) + { + UE_LOG(LogPakFile, Display, TEXT("Sorting pak audit data by open order") ); + FileRevisions.KeySort([&]( const FString& A, const FString& B ) + { + return FindOpenOrder(A) < FindOpenOrder(B); + }); + DeletedRevisions.KeySort([&]( const FString& A, const FString& B ) + { + return FindOpenOrder(A) < FindOpenOrder(B); + }); + } + else + { + UE_LOG(LogPakFile, Display, TEXT("Sorting pak audit data by name") ); + FileRevisions.KeySort([]( const FString& A, const FString& B ) + { + return A.Compare(B, ESearchCase::IgnoreCase) < 0; + }); + DeletedRevisions.KeySort([]( const FString& A, const FString& B ) + { + return A.Compare(B, ESearchCase::IgnoreCase) < 0; + }); + } + + FString PreviousPatchDotChunk; + int NumSeeks = 0; + int NumReads = 0; + + UE_CLOG((CSVFileWriter!=nullptr),LogPakFile, Display, TEXT("Writing pak audit CSV file %s..."), *CSVFilename ); + WriteCSVLine( TEXT("AssetName,State,Pak,Prev.Pak,Rev,Prev.Rev,Size,AssetPath,Patch.Chunk,OpenOrder" ) ); for (auto Itr : FileRevisions) { const FString& AssetPath = Itr.Key; const FString AssetName = FPaths::GetCleanFilename(AssetPath); const FFilePakRevision* DeletedRevision = DeletedRevisions.Find(AssetPath); + + //look up the open order for this file + FString OpenOrderText = ""; + uint64 OpenOrder = FindOpenOrder(AssetPath); + if( OpenOrder != UINT64_MAX ) + { + OpenOrderText = FString::Printf( TEXT("%llu"), OpenOrder ); + } + + //lookup patch.chunk value + FString PatchDotChunk = ""; + if( const FString* PatchDotChunkPtr = PakFilenameToPatchDotChunk.Find(Itr.Value.PakFilename) ) + { + PatchDotChunk = *PatchDotChunkPtr; + } + + bool bFileExists = true; if (DeletedRevision == nullptr) { if (bOnlyDeleted) @@ -1964,24 +2127,35 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& } else if (Itr.Value.PakPriority == HighestPakPriority) { - WriteCSVLine( FString::Printf( TEXT("%s,Fresh,%s,,%d,,%d,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Fresh,%s,,%d,,%d,%s,%s,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); } else { - WriteCSVLine( FString::Printf( TEXT("%s,Inherited,%s,,%d,,%d,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Inherited,%s,,%d,,%d,%s,%s,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); } } else if (DeletedRevision->PakPriority == Itr.Value.PakPriority) { - WriteCSVLine( FString::Printf( TEXT("%s,Moved,%s,%s,%d,,%d,%s"), *AssetName, *Itr.Value.PakFilename, *DeletedRevision->PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Moved,%s,%s,%d,,%d,%s,%s,%s"), *AssetName, *Itr.Value.PakFilename, *DeletedRevision->PakFilename, Itr.Value.PakPriority, Itr.Value.Size, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); } else if (DeletedRevision->PakPriority > Itr.Value.PakPriority) { - WriteCSVLine( FString::Printf( TEXT("%s,Deleted,%s,%s,%d,%d,,%s"), *AssetName, *DeletedRevision->PakFilename, *Itr.Value.PakFilename, DeletedRevision->PakPriority, Itr.Value.PakPriority, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Deleted,%s,%s,%d,%d,,%s,%s,%s"), *AssetName, *DeletedRevision->PakFilename, *Itr.Value.PakFilename, DeletedRevision->PakPriority, Itr.Value.PakPriority, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); + bFileExists = false; } else if (DeletedRevision->PakPriority < Itr.Value.PakPriority) { - WriteCSVLine( FString::Printf( TEXT("%s,Restored,%s,%s,%d,%d,%d,%s"), *AssetName, *Itr.Value.PakFilename, *DeletedRevision->PakFilename, Itr.Value.PakPriority, DeletedRevision->PakPriority, Itr.Value.Size, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Restored,%s,%s,%d,%d,%d,%s,%s,%s"), *AssetName, *Itr.Value.PakFilename, *DeletedRevision->PakFilename, Itr.Value.PakPriority, DeletedRevision->PakPriority, Itr.Value.Size, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); + } + + if( bFileExists && bSortByOrdering && bHasOpenOrder ) + { + NumReads++; + if( PreviousPatchDotChunk != PatchDotChunk ) + { + PreviousPatchDotChunk = PatchDotChunk; + NumSeeks++; + } } } @@ -1992,8 +2166,23 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& const FFilePakRevision* Revision = FileRevisions.Find(AssetPath); if (Revision == nullptr) { + //look up the open order for this file + FString OpenOrderText = ""; + uint64 OpenOrder = FindOpenOrder(AssetPath); + if( OpenOrder != UINT64_MAX ) + { + OpenOrderText = FString::Printf( TEXT("%llu"), OpenOrder ); + } + + //lookup patch.chunk value + FString PatchDotChunk = ""; + if( const FString* PatchDotChunkPtr = PakFilenameToPatchDotChunk.Find(Itr.Value.PakFilename) ) + { + PatchDotChunk = *PatchDotChunkPtr; + } + const FString AssetName = FPaths::GetCleanFilename(AssetPath); - WriteCSVLine( FString::Printf( TEXT("%s,Deleted,%s,Error,%d,,,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, *AssetPath ) ); + WriteCSVLine( FString::Printf( TEXT("%s,Deleted,%s,Error,%d,,,%s,%s,%s"), *AssetName, *Itr.Value.PakFilename, Itr.Value.PakPriority, *AssetPath, *PatchDotChunk, *OpenOrderText ) ); } } @@ -2005,6 +2194,13 @@ bool AuditPakFiles( const FString& InputPath, bool bOnlyDeleted, const FString& CSVFileWriter = NULL; } + + //write seek summary + if( bSortByOrdering && bHasOpenOrder && NumReads > 0 ) + { + UE_LOG( LogPakFile, Display, TEXT("%d guaranteed seeks out of %d files read (%.2f%%) with the given open order"), NumSeeks, NumReads, (NumSeeks*100.0f)/NumReads ); + } + return true; } @@ -2442,7 +2638,7 @@ bool GenerateHashForFile( FString Filename, FFileInfo& FileHash) bool GenerateHashesFromPak(const TCHAR* InPakFilename, const TCHAR* InDestPakFilename, TMap& FileHashes, bool bUseMountPoint, const TKeyChain& InKeyChain, int32& OutLowestSourcePakVersion, const bool bSigned ) { - OutLowestSourcePakVersion = FPakInfo::PakFile_Version_Initial-1; + OutLowestSourcePakVersion = FPakInfo::PakFile_Version_Invalid; TArray FoundFiles; IFileManager::Get().FindFiles(FoundFiles, InPakFilename, true, false); @@ -2478,7 +2674,7 @@ bool GenerateHashesFromPak(const TCHAR* InPakFilename, const TCHAR* InDestPakFil int32 FileCount = 0; //remember the lowest pak version for any patch paks - if( PakChunkIndex != -1 ) + if( PakPriority != -1 ) { OutLowestSourcePakVersion = FMath::Min( OutLowestSourcePakVersion, PakFile.GetInfo().Version ); } @@ -2578,11 +2774,16 @@ bool GenerateHashesFromPak(const TCHAR* InPakFilename, const TCHAR* InDestPakFil return true; } -bool FileIsIdentical(FString SourceFile, FString DestFilename, const FFileInfo* Hash) +bool FileIsIdentical(FString SourceFile, FString DestFilename, const FFileInfo* Hash, int64* DestSizeOut = nullptr) { int64 SourceTotalSize = Hash ? Hash->FileSize : IFileManager::Get().FileSize(*SourceFile); int64 DestTotalSize = IFileManager::Get().FileSize(*DestFilename); + if (DestSizeOut != nullptr) + { + *DestSizeOut = DestTotalSize; + } + if (SourceTotalSize != DestTotalSize) { // file size doesn't match @@ -2627,7 +2828,23 @@ bool FileIsIdentical(FString SourceFile, FString DestFilename, const FFileInfo* return true; } -void RemoveIdenticalFiles( TArray& FilesToPak, const FString& SourceDirectory, const TMap& FileHashes ) +int32 CountBitToggles(const TBitArray<>& BitArray) +{ + int32 ChangeCount = 0; + bool bPrevBit = false; + for (int i = 0; i < BitArray.Num(); i++) + { + bool bCurrentBit = BitArray[i]; + if (i == 0 || bCurrentBit != bPrevBit) + { + ChangeCount++; + } + bPrevBit = bCurrentBit; + } + return ChangeCount; +} + +void RemoveIdenticalFiles( TArray& FilesToPak, const FString& SourceDirectory, const TMap& FileHashes, int64 SeekOptMaxGapSizeBytes, bool bSeekOptUseOrder) { FString HashFilename = SourceDirectory / TEXT("Hashes.txt"); @@ -2637,18 +2854,50 @@ void RemoveIdenticalFiles( TArray& FilesToPak, const FString& Sou FFileHelper::LoadFileToString(EntireFile, *HashFilename); } - TArray FilesToRemove; + TBitArray<> IncludeFilesMask; + IncludeFilesMask.Add(true, FilesToPak.Num()); - for ( int I = FilesToPak.Num()-1; I >= 0; --I ) + TMap SourceFileToIndex; + for (int i = 0; i < FilesToPak.Num(); i++) { - const auto& NewFile = FilesToPak[I]; + SourceFileToIndex.Add(FilesToPak[i].Source, i); + } + + // Generate the index mapping from UExp to corresponding UAsset (and vice versa) + TArray UAssetToUexpMapping; + UAssetToUexpMapping.Empty(FilesToPak.Num()); + for (int i = 0; i FileSizes; + FileSizes.AddDefaulted(FilesToPak.Num()); + + // Mark files to remove if they're unchanged + for (int i= 0; i& FilesToPak, const FString& Sou } // uexp files are always handled with their corresponding uasset file + bool bForceInclude = false; if (!FPaths::GetExtension(SourceFilename).Equals("uexp", ESearchCase::IgnoreCase)) { - bool bForceInclude = FoundFileHash && FoundFileHash->bForceInclude; + bForceInclude = FoundFileHash && FoundFileHash->bForceInclude; + } FString DestFilename = NewFile.Source; - if (!bForceInclude && FileIsIdentical(SourceFilename, DestFilename, FoundFileHash)) + if (!bForceInclude && FileIsIdentical(SourceFilename, DestFilename, FoundFileHash, &FileSizes[i])) + { + UE_LOG(LogPakFile, Display, TEXT("Source file %s matches dest file %s and will not be included in patch"), *SourceFilename, *DestFilename); + // remove from the files to pak list + IncludeFilesMask[i] = false; + } + } + + // Add corresponding UExp/UBulk files to the patch if one is included but not the other (uassets and uexp files must be in the same pak) + for (int i = 0; i < FilesToPak.Num(); i++) + { + int32 CounterpartFileIndex= UAssetToUexpMapping[i]; + if (CounterpartFileIndex != -1) + { + if (IncludeFilesMask[i] != IncludeFilesMask[CounterpartFileIndex]) { - // Check for uexp files only for uasset files - FString Ext(FPaths::GetExtension(SourceFilename)); - if (Ext.Equals("uasset", ESearchCase::IgnoreCase) || Ext.Equals("umap", ESearchCase::IgnoreCase)) - { - FString UexpSourceFilename = FPaths::ChangeExtension(SourceFilename, "uexp"); - FString UexpSourceFileNoMountPoint = FPaths::ChangeExtension(SourceFileNoMountPoint, "uexp"); - - const FFileInfo* UexpFoundFileHash = FileHashes.Find(UexpSourceFileNoMountPoint); - if (!UexpFoundFileHash) - { - UexpFoundFileHash = FileHashes.Find(FPaths::ChangeExtension(NewFile.Dest, "uexp")); - } - - if (!UexpFoundFileHash) - { - UE_LOG(LogPakFile, Display, TEXT("Didn't find hash for %s No mount %s"), *UexpSourceFilename, *UexpSourceFileNoMountPoint); - } - - if (UexpFoundFileHash || IFileManager::Get().FileExists(*UexpSourceFilename)) - { - - FString UexpDestFilename = FPaths::ChangeExtension(NewFile.Source, "uexp"); - if (!FileIsIdentical(UexpSourceFilename, UexpDestFilename, UexpFoundFileHash)) - { - UE_LOG(LogPakFile, Display, TEXT("%s not identical for %s. Including both files in patch."), *UexpSourceFilename, *SourceFilename); - continue; - } - // Add this file to the list to be removed from FilesToPak after we finish processing (since this file was found at random within - // the list we cannot remove it or we'll mess up our containing for loop) - FilesToRemove.Add(UexpDestFilename); - } - } - - UE_LOG(LogPakFile, Display, TEXT("Source file %s matches dest file %s and will not be included in patch"), *SourceFilename, *DestFilename); - // remove from the files to pak list - FilesToPak.RemoveAt(I); + UE_LOG(LogPakFile, Display, TEXT("One of %s and %s is different from source, so both will be included in patch"), *FilesToPak[i].Source, *FilesToPak[CounterpartFileIndex].Source); + IncludeFilesMask[i] = true; + IncludeFilesMask[CounterpartFileIndex] = true; } } } - // Clean up uexp files that were marked for removal, assume files may only be listed one in FilesToPak - for (int FileIndexToRemove = 0; FileIndexToRemove < FilesToRemove.Num(); FileIndexToRemove++) - { - const FPakInputPair FileSourceToRemove(FilesToRemove[FileIndexToRemove], ""); - FilesToPak.RemoveSingle(FileSourceToRemove); + if (SeekOptMaxGapSizeBytes > 0) + { + UE_LOG(LogPakFile, Display, TEXT("Patch seek optimization - filling gaps up to %dKB"), SeekOptMaxGapSizeBytes/1024); + + int32 PatchContiguousBlockCountOriginal = CountBitToggles(IncludeFilesMask); + int64 MaxGapSizeBytes = 0; + int64 OriginalPatchSize = 0; + int64 SizeIncrease = 0; + int64 CurrentOffset = 0; + int64 CurrentPatchOffset = 0; + int64 CurrentGapSize = 0; + bool bPrevKeepFile = false; + uint64 PrevOrder=MAX_uint64; + int32 OriginalKeepCount = 0; + int32 LastKeepIndex = -1; + bool bCurrentGapIsUnbroken = true; + int32 PatchFilesAddedCount = 0; + int32 OriginalPatchFileCount = 0; + for (int i = 0; i < FilesToPak.Num(); i++) + { + bool bKeepFile = IncludeFilesMask[i]; + uint64 Order = FilesToPak[i].SuggestedOrder; + if (bKeepFile) + { + OriginalPatchFileCount++; + OriginalPatchSize += FileSizes[i]; + } + + if (Order == MAX_uint64) + { + // Skip unordered files + continue; + } + CurrentOffset += FileSizes[i]; + if (bKeepFile) + { + OriginalKeepCount++; + CurrentPatchOffset = CurrentOffset; + } + else + { + if (OriginalKeepCount > 0) + { + CurrentGapSize = CurrentOffset - CurrentPatchOffset; + } + } + + // Detect gaps in the file order. No point in removing those gaps because it won't affect seeks + if (bCurrentGapIsUnbroken && Order != PrevOrder + 1) + { + bCurrentGapIsUnbroken = false; + } + + // If we're keeping this file but not the last one, check if the gap size is small enough to bring over unchanged assets + if (bKeepFile && !bPrevKeepFile && CurrentGapSize > 0 ) + { + if ( CurrentGapSize <= SeekOptMaxGapSizeBytes) + { + if (bCurrentGapIsUnbroken || bSeekOptUseOrder==false) + { + // Mark the files in the gap to keep, even though they're unchanged + for (int j = LastKeepIndex + 1; j < i; j++) + { + IncludeFilesMask[j] = true; + SizeIncrease += FileSizes[j]; + PatchFilesAddedCount++; + } + } + } + bCurrentGapIsUnbroken = true; + } + bPrevKeepFile = bKeepFile; + if (bKeepFile) + { + LastKeepIndex = i; + } + PrevOrder = Order; + } + + // Add corresponding UExp/UBulk files to the patch if either is included but not the other + for (int i = 0; i < FilesToPak.Num(); i++) + { + int32 CounterpartFileIndex = UAssetToUexpMapping[i]; + if (CounterpartFileIndex != -1) + { + if (IncludeFilesMask[i] != IncludeFilesMask[CounterpartFileIndex]) + { + if ( !IncludeFilesMask[i] ) + { + IncludeFilesMask[i] = true; + SizeIncrease += FileSizes[i]; + } + else + { + IncludeFilesMask[CounterpartFileIndex] = true; + SizeIncrease += FileSizes[CounterpartFileIndex]; + } + PatchFilesAddedCount++; + } + } + } + + double OriginalSizeMB = double(OriginalPatchSize) / 1024.0 / 1024.0; + double SizeIncreaseMB = double(SizeIncrease) / 1024.0 / 1024.0; + double TotalSizeMB = OriginalSizeMB + SizeIncreaseMB; + double SizeIncreasePercent = 100.0 * SizeIncreaseMB / OriginalSizeMB; + if (PatchFilesAddedCount == 0) + { + UE_LOG(LogPakFile, Display, TEXT("Patch seek optimization did not modify patch pak size (no additional files added)"), OriginalSizeMB, TotalSizeMB, SizeIncreasePercent); + } + else + { + UE_LOG(LogPakFile, Display, TEXT("Patch seek optimization increased estimated patch pak size from %.2fMB to %.2fMB (+%.1f%%)"), OriginalSizeMB, TotalSizeMB, SizeIncreasePercent); + UE_LOG(LogPakFile, Display, TEXT("Total files added : %d (of %d)"), PatchFilesAddedCount, OriginalPatchFileCount + PatchFilesAddedCount); + } + UE_LOG(LogPakFile, Display, TEXT("Contiguous block count pre-optimization: %d"), PatchContiguousBlockCountOriginal); + + int32 PatchContiguousBlockCountFinal = CountBitToggles(IncludeFilesMask); + UE_LOG(LogPakFile, Display, TEXT("Contiguous block count final: %d"), PatchContiguousBlockCountFinal); } + + // Compress the array while preserving the order, removing the files we marked to remove + int32 WriteIndex = 0; + for ( int ReadIndex=0; ReadIndex& InDeleteRecords, TMap& InExistingPackagedFileHashes, const FString& InInputPath, const TArray& InFilesToPak, int32 CurrentPatchChunkIndex, bool bSigned ) @@ -3019,6 +3377,12 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) return false; } + UE_LOG(LogPakFile, Display, TEXT("Running UnrealPak in batch mode with commands:")); + for (int i = 0; i < Commands.Num(); i++) + { + UE_LOG(LogPakFile, Display, TEXT("[%d] : %s"), i, *Commands[i]); + } + TAtomic Result(true); ParallelFor(Commands.Num(), [&Commands, &Result](int32 Idx) { if (!ExecuteUnrealPak(*Commands[Idx])) { Result = false; } }); return Result; @@ -3060,6 +3424,10 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) if (FParse::Param(CmdLine, TEXT("List"))) { + + FPakCommandLineParameters CmdLineParameters; + PreProcessCommandline(CmdLine, CmdLineParameters); + if (NonOptionArguments.Num() != 1) { UE_LOG(LogPakFile, Error, TEXT("Incorrect arguments. Expected: -List [-SizeFilter=N] [-Signed]")); @@ -3105,6 +3473,9 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) if (FParse::Param(CmdLine, TEXT("Extract"))) { + FPakCommandLineParameters CmdLineParameters; + PreProcessCommandline(CmdLine, CmdLineParameters); + if (NonOptionArguments.Num() != 2) { UE_LOG(LogPakFile, Error, TEXT("Incorrect arguments. Expected: -Extract ")); @@ -3118,7 +3489,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) FString Filter; FString DestPath = NonOptionArguments[1]; - bUseFilter = FParse::Value(FCommandLine::Get(), TEXT("Filter="), Filter); + bUseFilter = FParse::Value(CmdLine, TEXT("Filter="), Filter); bool bExtractToMountPoint = FParse::Param(CmdLine, TEXT("ExtractToMountPoint")); TMap EmptyMap; return ExtractFilesFromPak(*PakFilename, EmptyMap, *DestPath, bExtractToMountPoint, KeyChain, bSigned, bUseFilter ? &Filter : nullptr); @@ -3128,19 +3499,33 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) { if (NonOptionArguments.Num() != 1) { - UE_LOG(LogPakFile, Error, TEXT("Incorrect arguments. Expected: -AuditFiles -CSV= [-OnlyDeleted]")); + UE_LOG(LogPakFile, Error, TEXT("Incorrect arguments. Expected: -AuditFiles -CSV= [-OnlyDeleted] [-Order=] [-SortByOrdering]")); return false; } - FString PakFilename = GetPakPath(*NonOptionArguments[0], false); + FString PakFilenames = *NonOptionArguments[0]; + FPaths::MakeStandardFilename(PakFilenames); FString CSVFilename; FParse::Value( CmdLine, TEXT("CSV="), CSVFilename ); bool bOnlyDeleted = FParse::Param( CmdLine, TEXT("OnlyDeleted") ); bool bSigned = FParse::Param(CmdLine, TEXT("signed")); + bool bSortByOrdering = FParse::Param(CmdLine, TEXT("SortByOrdering")); - return AuditPakFiles(*PakFilename, bOnlyDeleted, CSVFilename, bSigned); + TMap OrderMap; + FString ResponseFile; + if (FParse::Value(CmdLine, TEXT("-order="), ResponseFile) && !ProcessOrderFile(*ResponseFile, OrderMap)) + { + return false; + } + FString SecondaryResponseFile; + if (FParse::Value(CmdLine, TEXT("-secondaryOrder="), SecondaryResponseFile) && !ProcessOrderFile(*SecondaryResponseFile, OrderMap, true, OrderMap.Num())) + { + return false; + } + + return AuditPakFiles(*PakFilenames, bOnlyDeleted, CSVFilename, bSigned, OrderMap, bSortByOrdering ); } if (FParse::Param(CmdLine, TEXT("WhatsAtOffset"))) @@ -3167,7 +3552,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) return ListFilesAtOffset( *PakFilename, Offsets, bSigned ); } - if (FParse::Param(FCommandLine::Get(), TEXT("GeneratePIXMappingFile"))) + if (FParse::Param(CmdLine, TEXT("GeneratePIXMappingFile"))) { if (NonOptionArguments.Num() != 1) { @@ -3190,7 +3575,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) } FString OutputPath; - FParse::Value(FCommandLine::Get(), TEXT("OutputPath="), OutputPath); + FParse::Value(CmdLine, TEXT("OutputPath="), OutputPath); return GeneratePIXMappingFile(PakFileList, OutputPath); } @@ -3275,6 +3660,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) // List of all items to add to pak file TArray Entries; FPakCommandLineParameters CmdLineParameters; + PreProcessCommandline(CmdLine, CmdLineParameters); ProcessCommandLine(CmdLine, NonOptionArguments, Entries, CmdLineParameters); TMap OrderMap; @@ -3284,6 +3670,12 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) return false; } + FString SecondaryResponseFile; + if (FParse::Value(CmdLine, TEXT("-secondaryOrder="), SecondaryResponseFile) && !ProcessOrderFile(*SecondaryResponseFile, OrderMap, true, OrderMap.Num())) + { + return false; + } + if (Entries.Num() == 0) { UE_LOG(LogPakFile, Error, TEXT("No files specified to add to pak file.")); @@ -3356,7 +3748,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) FilesToAdd.Append(DeleteRecords); // if we are generating a patch here we remove files which are already shipped... - RemoveIdenticalFiles(FilesToAdd, CmdLineParameters.SourcePatchDiffDirectory, SourceFileHashes); + RemoveIdenticalFiles(FilesToAdd, CmdLineParameters.SourcePatchDiffDirectory, SourceFileHashes, CmdLineParameters.PatchSeekOptMaxGapSize, CmdLineParameters.PatchSeekOptUseOrder); } @@ -3383,7 +3775,7 @@ bool ExecuteUnrealPak(const TCHAR* CmdLine) UE_LOG(LogPakFile, Error, TEXT(" UnrealPak GenerateKeys=")); UE_LOG(LogPakFile, Error, TEXT(" UnrealPak GeneratePrimeTable= [-TableMax=]")); UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -diff")); - UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -AuditFiles [-OnlyDeleted] [-CSV=]")); + UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -AuditFiles [-OnlyDeleted] [-CSV=] [-order=] [-SortByOrdering]")); UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -WhatsAtOffset [offset1] [offset2] [offset3] [...]")); UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -GeneratePIXMappingFile -OutputPath=")); UE_LOG(LogPakFile, Error, TEXT(" UnrealPak -TestEncryption")); diff --git a/Engine/Source/Developer/ShaderFormatOpenGL/Private/OpenGLShaderCompiler.cpp b/Engine/Source/Developer/ShaderFormatOpenGL/Private/OpenGLShaderCompiler.cpp index 5df1b0f56910..5fd8b682e32b 100644 --- a/Engine/Source/Developer/ShaderFormatOpenGL/Private/OpenGLShaderCompiler.cpp +++ b/Engine/Source/Developer/ShaderFormatOpenGL/Private/OpenGLShaderCompiler.cpp @@ -1468,7 +1468,7 @@ void FOpenGLFrontend::CompileShader(const FShaderCompilerInput& Input,FShaderCom bIsSM5 ? HSF_DomainShader : HSF_InvalidFrequency, HSF_PixelShader, IsES2Platform(Version) ? HSF_InvalidFrequency : HSF_GeometryShader, - bIsSM5 ? HSF_ComputeShader : HSF_InvalidFrequency + RHISupportsComputeShaders(Input.Target.GetPlatform()) ? HSF_ComputeShader : HSF_InvalidFrequency }; const EHlslShaderFrequency Frequency = FrequencyTable[Input.Target.Frequency]; diff --git a/Engine/Source/Developer/SimplygonMeshReduction/Private/SimplygonMeshReduction.cpp b/Engine/Source/Developer/SimplygonMeshReduction/Private/SimplygonMeshReduction.cpp index 661a0b3f8765..c5c2d7002590 100644 --- a/Engine/Source/Developer/SimplygonMeshReduction/Private/SimplygonMeshReduction.cpp +++ b/Engine/Source/Developer/SimplygonMeshReduction/Private/SimplygonMeshReduction.cpp @@ -615,6 +615,42 @@ public: return true; } + /** + * Returns true if mesh reduction is active. Active mean there will be a reduction of the vertices or triangle number + */ + virtual bool IsReductionActive(const FMeshReductionSettings &ReductionSettings) const + { + float Threshold_One = (1.0f - KINDA_SMALL_NUMBER); + float Threshold_Zero = (0.0f + KINDA_SMALL_NUMBER); + return ReductionSettings.PercentTriangles < Threshold_One || ReductionSettings.MaxDeviation > Threshold_Zero; + } + + virtual bool IsReductionActive(const FSkeletalMeshOptimizationSettings &ReductionSettings) const + { + float Threshold_One = (1.0f - KINDA_SMALL_NUMBER); + float Threshold_Zero = (0.0f + KINDA_SMALL_NUMBER); + switch (ReductionSettings.ReductionMethod) + { + case SkeletalMeshOptimizationType::SMOT_NumOfTriangles: + { + return ReductionSettings.NumOfTrianglesPercentage < Threshold_One; + } + break; + case SkeletalMeshOptimizationType::SMOT_MaxDeviation: + { + return ReductionSettings.MaxDeviationPercentage > Threshold_Zero; + } + break; + case SkeletalMeshOptimizationType::SMOT_TriangleOrDeviation: + { + return ReductionSettings.NumOfTrianglesPercentage < Threshold_One || ReductionSettings.MaxDeviationPercentage > Threshold_Zero; + } + break; + } + + return false; + } + static void Destroy() { if (GSimplygonSDKDLLHandle != nullptr) diff --git a/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.cpp b/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.cpp index 4b03778d307a..194acbadc72f 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.cpp +++ b/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.cpp @@ -111,6 +111,11 @@ FText FLiveWidgetReflectorNode::GetWidgetType() const return FWidgetReflectorNodeUtils::GetWidgetType(Widget.Pin()); } +FText FLiveWidgetReflectorNode::GetWidgetTypeAndShortName() const +{ + return FWidgetReflectorNodeUtils::GetWidgetTypeAndShortName(Widget.Pin()); +} + FText FLiveWidgetReflectorNode::GetWidgetVisibilityText() const { return FWidgetReflectorNodeUtils::GetWidgetVisibilityText(Widget.Pin()); @@ -191,6 +196,7 @@ FSnapshotWidgetReflectorNode::FSnapshotWidgetReflectorNode() FSnapshotWidgetReflectorNode::FSnapshotWidgetReflectorNode(const FArrangedWidget& InArrangedWidget) : FWidgetReflectorNodeBase(InArrangedWidget) , CachedWidgetType(FWidgetReflectorNodeUtils::GetWidgetType(InArrangedWidget.Widget)) + , CachedWidgetTypeAndShortName(FWidgetReflectorNodeUtils::GetWidgetTypeAndShortName(InArrangedWidget.Widget)) , CachedWidgetVisibilityText(FWidgetReflectorNodeUtils::GetWidgetVisibilityText(InArrangedWidget.Widget)) , bCachedWidgetFocusable(FWidgetReflectorNodeUtils::GetWidgetFocusable(InArrangedWidget.Widget)) , CachedWidgetClippingText(FWidgetReflectorNodeUtils::GetWidgetClippingText(InArrangedWidget.Widget)) @@ -220,6 +226,11 @@ FText FSnapshotWidgetReflectorNode::GetWidgetType() const return CachedWidgetType; } +FText FSnapshotWidgetReflectorNode::GetWidgetTypeAndShortName() const +{ + return CachedWidgetTypeAndShortName; +} + FText FSnapshotWidgetReflectorNode::GetWidgetVisibilityText() const { return CachedWidgetVisibilityText; @@ -586,6 +597,28 @@ FText FWidgetReflectorNodeUtils::GetWidgetType(const TSharedPtr& InWidg return (InWidget.IsValid()) ? FText::FromString(InWidget->GetTypeAsString()) : FText::GetEmpty(); } +FText FWidgetReflectorNodeUtils::GetWidgetTypeAndShortName(const TSharedPtr& InWidget) +{ + if (InWidget.IsValid()) + { + FText WidgetType = GetWidgetType(InWidget); + + // UMG widgets have meta-data to help track them + TSharedPtr MetaData = InWidget->GetMetaData(); + if (MetaData.IsValid()) + { + if (MetaData->Name != NAME_None) + { + return FText::Format(LOCTEXT("WidgetTypeAndName", "{0} ({1})"), WidgetType, FText::FromName(MetaData->Name)); + } + } + + return WidgetType; + } + + return FText::GetEmpty(); +} + FText FWidgetReflectorNodeUtils::GetWidgetVisibilityText(const TSharedPtr& InWidget) { return (InWidget.IsValid()) ? FText::FromString(InWidget->GetVisibility().ToString()) : FText::GetEmpty(); diff --git a/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.h b/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.h index 5fb35d5f50b7..14caa621422f 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.h +++ b/Engine/Source/Developer/SlateReflector/Private/Models/WidgetReflectorNode.h @@ -58,6 +58,11 @@ public: * @return The type string for the widget we were initialized from */ virtual FText GetWidgetType() const = 0; + + /** + * @return The type string for the widget we were initialized from + */ + virtual FText GetWidgetTypeAndShortName() const = 0; /** * @return The visibility string for the widget we were initialized from @@ -211,6 +216,7 @@ public: virtual EWidgetReflectorNodeType GetNodeType() const override; virtual TSharedPtr GetLiveWidget() const override; virtual FText GetWidgetType() const override; + virtual FText GetWidgetTypeAndShortName() const override; virtual FText GetWidgetVisibilityText() const override; virtual FText GetWidgetClippingText() const override; virtual bool GetWidgetFocusable() const override; @@ -260,6 +266,7 @@ public: virtual EWidgetReflectorNodeType GetNodeType() const override; virtual TSharedPtr GetLiveWidget() const override; virtual FText GetWidgetType() const override; + virtual FText GetWidgetTypeAndShortName() const override; virtual FText GetWidgetVisibilityText() const override; virtual FText GetWidgetClippingText() const override; virtual bool GetWidgetFocusable() const override; @@ -294,6 +301,9 @@ private: /** The type string of the widget at the point it was passed to Initialize */ FText CachedWidgetType; + /** */ + FText CachedWidgetTypeAndShortName; + /** The visibility string of the widget at the point it was passed to Initialize */ FText CachedWidgetVisibilityText; @@ -401,6 +411,11 @@ public: * @return The type string for the given widget */ static FText GetWidgetType(const TSharedPtr& InWidget); + + /** + * @return The type string combined with a short name (if any) for the given widget + */ + static FText GetWidgetTypeAndShortName(const TSharedPtr& InWidget); /** * @return The current visibility string for the given widget diff --git a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.cpp b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.cpp index 2a379d0106de..ab8988ab507d 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.cpp +++ b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.cpp @@ -1,6 +1,9 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "Widgets/SWidgetEventLog.h" + +#if WITH_SLATE_DEBUGGING + #include "Debugging/SlateDebugging.h" #include "Widgets/SBoxPanel.h" #include "MessageLogModule.h" @@ -32,20 +35,16 @@ SWidgetEventLog::~SWidgetEventLog() void SWidgetEventLog::RemoveListeners() { -#if WITH_SLATE_DEBUGGING FSlateDebugging::InputEvent.RemoveAll(this); FSlateDebugging::FocusEvent.RemoveAll(this); -#endif } void SWidgetEventLog::UpdateListeners() { RemoveListeners(); -#if WITH_SLATE_DEBUGGING FSlateDebugging::InputEvent.AddSP(this, &SWidgetEventLog::OnInputEvent); FSlateDebugging::FocusEvent.AddSP(this, &SWidgetEventLog::OnFocusEvent); -#endif } void SWidgetEventLog::OnInputEvent(const FSlateDebuggingInputEventArgs& EventArgs) @@ -114,3 +113,5 @@ void SWidgetEventLog::OnFocusEvent(const FSlateDebuggingFocusEventArgs& EventArg } #undef LOCTEXT_NAMESPACE + +#endif // WITH_SLATE_DEBUGGING \ No newline at end of file diff --git a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.h b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.h index e672e80015de..cbbab5195e96 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.h +++ b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetEventLog.h @@ -10,6 +10,8 @@ #include "Widgets/SCompoundWidget.h" #include "Debugging/SlateDebugging.h" +#if WITH_SLATE_DEBUGGING + /** * */ @@ -30,3 +32,5 @@ private: void OnInputEvent(const FSlateDebuggingInputEventArgs& EventArgs); void OnFocusEvent(const FSlateDebuggingFocusEventArgs& EventArgs); }; + +#endif // WITH_SLATE_DEBUGGING \ No newline at end of file diff --git a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflector.cpp b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflector.cpp index 5c8d7072cb54..edff72c04fff 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflector.cpp +++ b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflector.cpp @@ -148,7 +148,9 @@ private: TSharedRef SpawnWidgetDetails(const FSpawnTabArgs& Args); #endif +#if WITH_SLATE_DEBUGGING TSharedRef SpawnWidgetEvents(const FSpawnTabArgs& Args); +#endif void OnTabSpawned(const FName& TabIdentifier, const TSharedRef& SpawnedTab); @@ -523,8 +525,10 @@ void SWidgetReflector::Construct( const FArguments& InArgs ) } #endif +#if WITH_SLATE_DEBUGGING RegisterTrackedTabSpawner(WidgetReflectorTabID::WidgetEvents, FOnSpawnTab::CreateSP(this, &SWidgetReflector::SpawnWidgetEvents)) .SetDisplayName(LOCTEXT("WidgetEventsTab", "Widget Events")); +#endif this->ChildSlot [ @@ -949,6 +953,8 @@ TSharedRef SWidgetReflector::SpawnWidgetDetails(const FSpawnTabArgs& A #endif +#if WITH_SLATE_DEBUGGING + TSharedRef SWidgetReflector::SpawnWidgetEvents(const FSpawnTabArgs& Args) { auto OnTabClosed = [this](TSharedRef) @@ -962,6 +968,8 @@ TSharedRef SWidgetReflector::SpawnWidgetEvents(const FSpawnTabArgs& Ar ]; } +#endif + void SWidgetReflector::OnTabSpawned(const FName& TabIdentifier, const TSharedRef& SpawnedTab) { TWeakPtr* const ExistingTab = SpawnedTabs.Find(TabIdentifier); diff --git a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.cpp b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.cpp index 1b2e52426e32..bfdb0e61cac5 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.cpp +++ b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.cpp @@ -29,6 +29,7 @@ void SReflectorTreeWidgetItem::Construct(const FArguments& InArgs, const TShared check(WidgetInfo.IsValid()); CachedWidgetType = WidgetInfo->GetWidgetType(); + CachedWidgetTypeAndShortName = WidgetInfo->GetWidgetTypeAndShortName(); CachedWidgetVisibility = WidgetInfo->GetWidgetVisibilityText(); CachedWidgetClipping = WidgetInfo->GetWidgetClippingText(); bCachedWidgetFocusable = WidgetInfo->GetWidgetFocusable(); @@ -61,7 +62,7 @@ TSharedRef SReflectorTreeWidgetItem::GenerateWidgetForColumn(const FNam .VAlign(VAlign_Center) [ SNew(STextBlock) - .Text(this, &SReflectorTreeWidgetItem::GetWidgetType) + .Text(this, &SReflectorTreeWidgetItem::GetWidgetTypeAndShortName) .ColorAndOpacity(this, &SReflectorTreeWidgetItem::GetTint) ]; } diff --git a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.h b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.h index f4ca8feb5a47..f84a45cd24c3 100644 --- a/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.h +++ b/Engine/Source/Developer/SlateReflector/Private/Widgets/SWidgetReflectorTreeWidgetItem.h @@ -60,7 +60,12 @@ protected: { return CachedWidgetType; } - + + FText GetWidgetTypeAndShortName() const + { + return CachedWidgetTypeAndShortName; + } + virtual FString GetReadableLocation() const override { return CachedReadableLocation.ToString(); @@ -111,6 +116,7 @@ private: TSharedPtr WidgetInfo; FText CachedWidgetType; + FText CachedWidgetTypeAndShortName; FText CachedWidgetVisibility; FText CachedWidgetClipping; bool bCachedWidgetFocusable; diff --git a/Engine/Source/Developer/TargetDeviceServices/Public/TargetDeviceServiceMessages.h b/Engine/Source/Developer/TargetDeviceServices/Public/TargetDeviceServiceMessages.h index 12c6b4dbbf05..62bc1b312914 100644 --- a/Engine/Source/Developer/TargetDeviceServices/Public/TargetDeviceServiceMessages.h +++ b/Engine/Source/Developer/TargetDeviceServices/Public/TargetDeviceServiceMessages.h @@ -230,9 +230,9 @@ struct FTargetDeviceServiceTerminateLaunchedProcess { GENERATED_USTRUCT_BODY() - /** Holds the variant identifier of the target device to use. */ - UPROPERTY(EditAnywhere, Category = "Message") - FName Variant; + /** Holds the variant identifier of the target device to use. */ + UPROPERTY(EditAnywhere, Category = "Message") + FName Variant; /** * Holds the identifier of the application to launch. @@ -243,7 +243,7 @@ struct FTargetDeviceServiceTerminateLaunchedProcess * services as result of successful deployment transactions. */ UPROPERTY(EditAnywhere, Category = "Message") - FString AppID; + FString AppID; /** Default constructor. */ FTargetDeviceServiceTerminateLaunchedProcess() { } diff --git a/Engine/Source/Developer/VulkanShaderFormat/Private/VulkanShaderCompiler.cpp b/Engine/Source/Developer/VulkanShaderFormat/Private/VulkanShaderCompiler.cpp index 90b0585e8c1c..9a400783b15a 100644 --- a/Engine/Source/Developer/VulkanShaderFormat/Private/VulkanShaderCompiler.cpp +++ b/Engine/Source/Developer/VulkanShaderFormat/Private/VulkanShaderCompiler.cpp @@ -1824,7 +1824,8 @@ static bool CallHlslcc(const FString& PreprocessedShader, FVulkanBindingTable& B void DoCompileVulkanShader(const FShaderCompilerInput& Input, FShaderCompilerOutput& Output, const class FString& WorkingDirectory, EVulkanShaderVersion Version) { - check(IsVulkanPlatform((EShaderPlatform)Input.Target.Platform)); + const EShaderPlatform ShaderPlatform = (EShaderPlatform)Input.Target.Platform; + check(IsVulkanPlatform(ShaderPlatform)); //if (GUseExternalShaderCompiler) //{ @@ -1844,7 +1845,7 @@ void DoCompileVulkanShader(const FShaderCompilerInput& Input, FShaderCompilerOut bIsSM5 ? HSF_DomainShader : HSF_InvalidFrequency, HSF_PixelShader, (bIsSM4 || bIsSM5) ? HSF_GeometryShader : HSF_InvalidFrequency, - bIsSM5 ? HSF_ComputeShader : HSF_InvalidFrequency + RHISupportsComputeShaders(ShaderPlatform) ? HSF_ComputeShader : HSF_InvalidFrequency }; const EHlslShaderFrequency Frequency = FrequencyTable[Input.Target.Frequency]; diff --git a/Engine/Source/Developer/Windows/ShaderFormatD3D/Private/D3D11ShaderCompiler.cpp b/Engine/Source/Developer/Windows/ShaderFormatD3D/Private/D3D11ShaderCompiler.cpp index 6de56d0a0e47..a085abe876ef 100644 --- a/Engine/Source/Developer/Windows/ShaderFormatD3D/Private/D3D11ShaderCompiler.cpp +++ b/Engine/Source/Developer/Windows/ShaderFormatD3D/Private/D3D11ShaderCompiler.cpp @@ -154,7 +154,8 @@ static const TCHAR* GetShaderProfileName(FShaderTarget Target) { checkSlow(Target.Frequency == SF_Vertex || Target.Frequency == SF_Pixel || - Target.Frequency == SF_Geometry); + Target.Frequency == SF_Geometry || + Target.Frequency == SF_Compute); //set defines and profiles for the appropriate shader paths switch(Target.Frequency) @@ -165,6 +166,8 @@ static const TCHAR* GetShaderProfileName(FShaderTarget Target) return TEXT("vs_5_0"); case SF_Geometry: return TEXT("gs_5_0"); + case SF_Compute: + return TEXT("cs_5_0"); } } diff --git a/Engine/Source/Editor/AnimationEditor/Private/AnimationEditor.cpp b/Engine/Source/Editor/AnimationEditor/Private/AnimationEditor.cpp index fa2e834e8e97..eedcb8012939 100644 --- a/Engine/Source/Editor/AnimationEditor/Private/AnimationEditor.cpp +++ b/Engine/Source/Editor/AnimationEditor/Private/AnimationEditor.cpp @@ -936,20 +936,12 @@ void FAnimationEditor::FillExportAssetMenu(FMenuBuilder& MenuBuilder) const FRichCurve* FindOrAddCurve(UCurveTable* CurveTable, FName CurveName) { - FRichCurve* Curve = nullptr; - // Grab existing curve (if present) - FRichCurve** ExistingCurvePtr = CurveTable->RowMap.Find(CurveName); - if (ExistingCurvePtr) + FRichCurve* Curve = CurveTable->FindRichCurve(CurveName, FString()); + if (Curve == nullptr) { - check(*ExistingCurvePtr); - Curve = *ExistingCurvePtr; - } - // Or allocate new curve - else - { - Curve = new FRichCurve(); - CurveTable->RowMap.Add(CurveName, Curve); + // Or allocate new curve + Curve = &CurveTable->AddRichCurve(CurveName); } return Curve; diff --git a/Engine/Source/Editor/AudioEditor/Private/Factories/ReimportSoundFactory.cpp b/Engine/Source/Editor/AudioEditor/Private/Factories/ReimportSoundFactory.cpp index 22744ba0240f..6bd8def79a21 100644 --- a/Engine/Source/Editor/AudioEditor/Private/Factories/ReimportSoundFactory.cpp +++ b/Engine/Source/Editor/AudioEditor/Private/Factories/ReimportSoundFactory.cpp @@ -87,6 +87,9 @@ EReimportResult::Type UReimportSoundFactory::Reimport(UObject* Obj) UE_LOG(LogAudioEditor, Log, TEXT("-- imported successfully")); SoundWave->AssetImportData->Update(Filename); + SoundWave->InvalidateCompressedData(); + SoundWave->FreeResources(); + SoundWave->UpdatePlatformData(); SoundWave->MarkPackageDirty(); SoundWave->bNeedsThumbnailGeneration = true; } diff --git a/Engine/Source/Editor/AudioEditor/Private/Factories/SoundFactory.cpp b/Engine/Source/Editor/AudioEditor/Private/Factories/SoundFactory.cpp index e289d243d02b..c6fcbc4c3368 100644 --- a/Engine/Source/Editor/AudioEditor/Private/Factories/SoundFactory.cpp +++ b/Engine/Source/Editor/AudioEditor/Private/Factories/SoundFactory.cpp @@ -11,11 +11,14 @@ #include "Sound/SoundNodeModulator.h" #include "Sound/SoundNodeAttenuation.h" #include "AudioDeviceManager.h" +#include "AudioDevice.h" +#include "AudioEditorModule.h" #include "Editor.h" #include "Misc/MessageDialog.h" #include "Misc/FeedbackContext.h" #include "EditorFramework/AssetImportData.h" + static bool bSoundFactorySuppressImportOverwriteDialog = false; static void InsertSoundNode(USoundCue* SoundCue, UClass* NodeClass, int32 NodeIndex) @@ -111,7 +114,7 @@ UObject* USoundFactory::FactoryCreateBinary UObject* Context, const TCHAR* FileType, const uint8*& Buffer, - const uint8* BufferEnd, + const uint8* BufferEnd, FFeedbackContext* Warn ) { @@ -155,6 +158,28 @@ UObject* USoundFactory::FactoryCreateBinary { // Will block internally on audio thread completing outstanding commands AudioDeviceManager->StopSoundsUsingResource(ExistingSound, &ComponentsToRestart); + + // Resource data is required to exist, if it hasn't been loaded yet, + // to properly flush compressed data. This allows the new version + // to be auditioned in the editor properly. + if (!ExistingSound->ResourceData) + { + FAudioDevice* AudioDevice = AudioDeviceManager->GetActiveAudioDevice(); + check(AudioDevice); + + FName RuntimeFormat = AudioDevice->GetRuntimeFormat(ExistingSound); + ExistingSound->InitAudioResource(RuntimeFormat); + } + + UE_LOG(LogAudioEditor, Log, TEXT("Stopping Sound Resources of Existing Sound")); + if (ComponentsToRestart.Num() > 0) + { + for (UAudioComponent* AudioComponent : ComponentsToRestart) + { + UE_LOG(LogAudioEditor, Log, TEXT("Component '%s' Stopped"), *AudioComponent->GetName()); + AudioComponent->Stop(); + } + } } bool bUseExistingSettings = bSoundFactorySuppressImportOverwriteDialog; diff --git a/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2.h b/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2.h index 022e6cbe2b37..c5f08104df40 100644 --- a/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2.h +++ b/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2.h @@ -282,6 +282,7 @@ class BLUEPRINTGRAPH_API UEdGraphSchema_K2 : public UEdGraphSchema static const FName PC_Class; // SubCategoryObject is the MetaClass of the Class passed thru this pin, or SubCategory can be 'self'. The DefaultValue string should always be empty, use DefaultObject. static const FName PC_SoftClass; static const FName PC_Int; + static const FName PC_Int64; static const FName PC_Float; static const FName PC_Name; static const FName PC_Delegate; // SubCategoryObject is the UFunction of the delegate signature diff --git a/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2_Actions.h b/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2_Actions.h index f4fc3f731009..f297d914f248 100644 --- a/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2_Actions.h +++ b/Engine/Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2_Actions.h @@ -216,7 +216,7 @@ struct FEdGraphSchemaAction_K2AddComponent : public FEdGraphSchemaAction_K2NewNo /** Class of component we want to add */ UPROPERTY() - TSubclassOf ComponentClass; + TSubclassOf ComponentClass; /** Option asset to assign to newly created component */ UPROPERTY() @@ -313,7 +313,7 @@ struct FEdGraphSchemaAction_K2AddCallOnActor : public FEdGraphSchemaAction_K2New /** Pointer to actors in level we want to call function on */ UPROPERTY() - TArray LevelActors; + TArray LevelActors; FEdGraphSchemaAction_K2AddCallOnActor() : FEdGraphSchemaAction_K2NewNode() diff --git a/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ActorBoundEvent.h b/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ActorBoundEvent.h index de2349ff6587..c68b4f2d482b 100644 --- a/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ActorBoundEvent.h +++ b/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ActorBoundEvent.h @@ -19,17 +19,17 @@ class UK2Node_ActorBoundEvent : public UK2Node_Event { GENERATED_UCLASS_BODY() - /** Delegate property name that this event is associated with */ - UPROPERTY() - FName DelegatePropertyName; + /** Delegate property name that this event is associated with */ + UPROPERTY() + FName DelegatePropertyName; /** Delegate property's owner class that this event is associated with */ UPROPERTY() - UClass* DelegateOwnerClass; + UClass* DelegateOwnerClass; /** The event that this event is bound to */ UPROPERTY() - class AActor* EventOwner; + class AActor* EventOwner; //~ Begin UObject Interface virtual void Serialize(FArchive& Ar) override; diff --git a/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ComponentBoundEvent.h b/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ComponentBoundEvent.h index 8716619f97f4..793f3bbff190 100644 --- a/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ComponentBoundEvent.h +++ b/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_ComponentBoundEvent.h @@ -18,17 +18,17 @@ class UK2Node_ComponentBoundEvent : public UK2Node_Event { GENERATED_UCLASS_BODY() - /** Delegate property name that this event is associated with */ - UPROPERTY() - FName DelegatePropertyName; + /** Delegate property name that this event is associated with */ + UPROPERTY() + FName DelegatePropertyName; /** Delegate property's owner class that this event is associated with */ UPROPERTY() - UClass* DelegateOwnerClass; + UClass* DelegateOwnerClass; /** Name of property in Blueprint class that pointer to component we want to bind to */ UPROPERTY() - FName ComponentPropertyName; + FName ComponentPropertyName; //~ Begin UObject Interface virtual bool Modify(bool bAlwaysMarkDirty = true) override; @@ -61,7 +61,7 @@ class UK2Node_ComponentBoundEvent : public UK2Node_Event private: /** Cached display name for the delegate property */ UPROPERTY() - FText DelegatePropertyDisplayName; + FText DelegatePropertyDisplayName; /** Constructing FText strings can be costly, so we cache the node's title */ FNodeTextCache CachedNodeTitle; diff --git a/Engine/Source/Editor/BlueprintGraph/Private/BlueprintGraphPanelPinFactory.cpp b/Engine/Source/Editor/BlueprintGraph/Private/BlueprintGraphPanelPinFactory.cpp index fbc246fd95d6..47af3d603362 100644 --- a/Engine/Source/Editor/BlueprintGraph/Private/BlueprintGraphPanelPinFactory.cpp +++ b/Engine/Source/Editor/BlueprintGraph/Private/BlueprintGraphPanelPinFactory.cpp @@ -52,7 +52,7 @@ TSharedPtr FBlueprintGraphPanelPinFactory::CreatePin(class UEdG { TArray> RowNames; /** Extract all the row names from the RowMap */ - for (TMap::TConstIterator Iterator(CurveTable->RowMap); Iterator; ++Iterator) + for (TMap::TConstIterator Iterator(CurveTable->GetRowMap()); Iterator; ++Iterator) { /** Create a simple array of the row names */ TSharedPtr RowNameItem = MakeShareable(new FName(Iterator.Key())); diff --git a/Engine/Source/Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp b/Engine/Source/Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp index ea58e74a7fb6..1b221a118836 100644 --- a/Engine/Source/Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp +++ b/Engine/Source/Editor/BlueprintGraph/Private/EdGraphSchema_K2.cpp @@ -2,6 +2,7 @@ #include "EdGraphSchema_K2.h" #include "BlueprintCompilationManager.h" +#include "Engine/Breakpoint.h" #include "Modules/ModuleManager.h" #include "UObject/Interface.h" #include "UObject/UnrealType.h" @@ -606,6 +607,7 @@ const FName UEdGraphSchema_K2::PC_Boolean(TEXT("bool")); const FName UEdGraphSchema_K2::PC_Byte(TEXT("byte")); const FName UEdGraphSchema_K2::PC_Class(TEXT("class")); const FName UEdGraphSchema_K2::PC_Int(TEXT("int")); +const FName UEdGraphSchema_K2::PC_Int64(TEXT("int64")); const FName UEdGraphSchema_K2::PC_Float(TEXT("float")); const FName UEdGraphSchema_K2::PC_Name(TEXT("name")); const FName UEdGraphSchema_K2::PC_Delegate(TEXT("delegate")); @@ -2931,6 +2933,10 @@ FLinearColor UEdGraphSchema_K2::GetPinTypeColor(const FEdGraphPinType& PinType) { return Settings->IntPinTypeColor; } + else if (TypeName == PC_Int64) + { + return Settings->Int64PinTypeColor; + } else if (TypeName == PC_Struct) { if (PinType.PinSubCategoryObject == VectorStruct) @@ -3035,11 +3041,11 @@ FText UEdGraphSchema_K2::GetPinDisplayName(const UEdGraphPin* Pin) const { FName DisplayFName(*DisplayName.ToString(), FNAME_Find); if ((DisplayFName == PN_Execute) || (DisplayFName == PN_Then)) - { - DisplayName = FText::GetEmpty(); + { + DisplayName = FText::GetEmpty(); + } } } - } if( GEditor && GetDefault()->bShowFriendlyNames ) { @@ -3359,6 +3365,10 @@ bool UEdGraphSchema_K2::GetPropertyCategoryInfo(const UProperty* TestProperty, F { OutCategory = PC_Float; } + else if (TestProperty->IsA()) + { + OutCategory = PC_Int64; + } else if (TestProperty->IsA()) { OutCategory = PC_Int; @@ -3619,7 +3629,8 @@ FText UEdGraphSchema_K2::GetCategoryText(const FName Category, const bool bForMe CategoryDescriptions.Add(PC_Boolean, LOCTEXT("BoolCategory","Boolean")); CategoryDescriptions.Add(PC_Byte, LOCTEXT("ByteCategory","Byte")); CategoryDescriptions.Add(PC_Class, LOCTEXT("ClassCategory","Class Reference")); - CategoryDescriptions.Add(PC_Int, LOCTEXT("IntCategory","Integer")); + CategoryDescriptions.Add(PC_Int, LOCTEXT("IntCategory", "Integer")); + CategoryDescriptions.Add(PC_Int64, LOCTEXT("Int64Category", "Integer64")); CategoryDescriptions.Add(PC_Float, LOCTEXT("FloatCategory","Float")); CategoryDescriptions.Add(PC_Name, LOCTEXT("NameCategory","Name")); CategoryDescriptions.Add(PC_Delegate, LOCTEXT("DelegateCategory","Delegate")); @@ -3801,6 +3812,7 @@ void UEdGraphSchema_K2::GetVariableTypeTree(TArray< TSharedPtr TypeTree.Add( MakeShareable( new FPinTypeTreeInfo(GetCategoryText(PC_Boolean, true), PC_Boolean, this, LOCTEXT("BooleanType", "True or false value")) ) ); TypeTree.Add( MakeShareable( new FPinTypeTreeInfo(GetCategoryText(PC_Byte, true), PC_Byte, this, LOCTEXT("ByteType", "8 bit number")) ) ); TypeTree.Add( MakeShareable( new FPinTypeTreeInfo(GetCategoryText(PC_Int, true), PC_Int, this, LOCTEXT("IntegerType", "Integer number")) ) ); + TypeTree.Add( MakeShareable( new FPinTypeTreeInfo(GetCategoryText(PC_Int64, true), PC_Int64, this, LOCTEXT("Integer64Type", "64 bit Integer number")) ) ); if (!bIndexTypesOnly) { TypeTree.Add(MakeShareable(new FPinTypeTreeInfo(GetCategoryText(PC_Float, true), PC_Float, this, LOCTEXT("FloatType", "Floating point number")))); @@ -4090,6 +4102,17 @@ bool UEdGraphSchema_K2::DefaultValueSimpleValidation(const FEdGraphPinType& PinT } } } + else if (PinCategory == PC_Int64) + { + if (!NewDefaultValue.IsEmpty()) + { + int64 ParsedInt64; + if (!FDefaultValueHelper::ParseInt64(NewDefaultValue, ParsedInt64)) + { + DVSV_RETURN_MSG(TEXT("Expected a valid number for an integer64 property")); + } + } + } else if (PinCategory == PC_Name) { // Anything is allowed @@ -4487,9 +4510,20 @@ void UEdGraphSchema_K2::HandleGraphBeingDeleted(UEdGraph& GraphBeingRemoved) con NodeToDelete->Modify(); NodeToDelete->DestroyNode(); } + + // likely tagged as modified by caller, but make sure: + Blueprint->Modify(); // Remove from the list of recently edited documents Blueprint->LastEditedDocuments.RemoveAll([&GraphBeingRemoved](const FEditedDocumentInfo& TestDoc) { return TestDoc.EditedObjectPath.ResolveObject() == &GraphBeingRemoved; }); + + // Remove any BPs that reference a node in this graph: + Blueprint->Breakpoints.RemoveAll( + [&GraphBeingRemoved](UBreakpoint* Breakpoint) + { + return !Breakpoint || (Breakpoint->GetLocation() && Breakpoint->GetLocation()->IsIn(&GraphBeingRemoved)); + } + ); } } @@ -4716,6 +4750,13 @@ bool UEdGraphSchema_K2::DoesDefaultValueMatchAutogenerated(const UEdGraphPin& In return true; } } + else if (InPin.PinType.PinCategory == PC_Int64) + { + if (FCString::Atoi64(*PinDefaultValue) == 0) + { + return true; + } + } else if (InPin.PinType.PinCategory == PC_Name) { return (PinDefaultValue == TEXT("None")); @@ -4851,6 +4892,10 @@ void UEdGraphSchema_K2::SetPinAutogeneratedDefaultValueBasedOnType(UEdGraphPin* { NewValue = TEXT("0"); } + else if (Pin->PinType.PinCategory == PC_Int64) + { + NewValue = TEXT("0"); + } else if (Pin->PinType.PinCategory == PC_Byte) { UEnum* EnumPtr = Cast(Pin->PinType.PinSubCategoryObject.Get()); @@ -4975,6 +5020,7 @@ void UEdGraphSchema_K2::ValidateExistingConnections(UEdGraphPin* Pin) namespace FSetVariableByNameFunctionNames { static const FName SetIntName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, SetIntPropertyByName)); + static const FName SetInt64Name(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, SetInt64PropertyByName)); static const FName SetByteName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, SetBytePropertyByName)); static const FName SetFloatName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, SetFloatPropertyByName)); static const FName SetBoolName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, SetBoolPropertyByName)); @@ -5031,6 +5077,10 @@ UFunction* UEdGraphSchema_K2::FindSetVariableByNameFunction(const FEdGraphPinTyp { SetFunctionName = FSetVariableByNameFunctionNames::SetIntName; } + else if (PinType.PinCategory == UEdGraphSchema_K2::PC_Int64) + { + SetFunctionName = FSetVariableByNameFunctionNames::SetInt64Name; + } else if(PinType.PinCategory == UEdGraphSchema_K2::PC_Byte) { SetFunctionName = FSetVariableByNameFunctionNames::SetByteName; diff --git a/Engine/Source/Editor/BlueprintGraph/Private/K2Node.cpp b/Engine/Source/Editor/BlueprintGraph/Private/K2Node.cpp index 69c5a052dc44..6aca883ede22 100644 --- a/Engine/Source/Editor/BlueprintGraph/Private/K2Node.cpp +++ b/Engine/Source/Editor/BlueprintGraph/Private/K2Node.cpp @@ -82,6 +82,7 @@ void UK2Node::Serialize(FArchive& Ar) if (!Pin->bDefaultValueIsIgnored && !Pin->DefaultValue.IsEmpty() ) { // If looking for references during save, expand any default values on the pins + // This is only reliable when saving in the editor, the cook case is handled below if (Ar.IsObjectReferenceCollector() && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct && Pin->PinType.PinSubCategoryObject.IsValid()) { UScriptStruct* Struct = Cast(Pin->PinType.PinSubCategoryObject.Get()); @@ -159,25 +160,37 @@ void UK2Node::FixupPinDefaultValues() } } - // Fix asset ptr pins - if (LinkerFrameworkVersion < FFrameworkObjectVersion::ChangeAssetPinsToString) + // Fix soft object ptr pins + if (GIsEditor || LinkerFrameworkVersion < FFrameworkObjectVersion::ChangeAssetPinsToString) { - bool bFoundPin = false; - for (int32 i = 0; i < Pins.Num() && !bFoundPin; ++i) + FSoftObjectPathSerializationScope SetPackage(GetOutermost()->GetFName(), NAME_None, ESoftObjectPathCollectType::AlwaysCollect, ESoftObjectPathSerializeType::SkipSerializeIfArchiveHasSize); + for (int32 i = 0; i < Pins.Num(); ++i) { UEdGraphPin* Pin = Pins[i]; - if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject || Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass) { - if (Pin->DefaultObject && Pin->DefaultValue.IsEmpty()) + // Fix old assetptr pins + if (LinkerFrameworkVersion < FFrameworkObjectVersion::ChangeAssetPinsToString) { - Pin->DefaultValue = Pin->DefaultObject->GetPathName(); - Pin->DefaultObject = nullptr; + if (Pin->DefaultObject && Pin->DefaultValue.IsEmpty()) + { + Pin->DefaultValue = Pin->DefaultObject->GetPathName(); + Pin->DefaultObject = nullptr; + } + } + + // In editor, fixup soft object ptrs on load on to handle redirects and finding refs for cooking + // We're not handling soft object ptrs inside FStructs because it's a rare edge case and would be a performance hit on load + if (GIsEditor && !Pin->DefaultValue.IsEmpty()) + { + FSoftObjectPath TempRef(Pin->DefaultValue); + TempRef.PostLoadPath(); + TempRef.PreSavePath(); + Pin->DefaultValue = TempRef.ToString(); } } } } - } FText UK2Node::GetToolTipHeading() const diff --git a/Engine/Source/Editor/BlueprintGraph/Private/K2Node_BaseAsyncTask.cpp b/Engine/Source/Editor/BlueprintGraph/Private/K2Node_BaseAsyncTask.cpp index 3c7e40700764..80cc65e4f1d3 100644 --- a/Engine/Source/Editor/BlueprintGraph/Private/K2Node_BaseAsyncTask.cpp +++ b/Engine/Source/Editor/BlueprintGraph/Private/K2Node_BaseAsyncTask.cpp @@ -114,6 +114,7 @@ void UK2Node_BaseAsyncTask::AllocateDefaultPins() { UEdGraphPin* ExecPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, Property->GetFName()); ExecPin->PinToolTip = Property->GetToolTipText().ToString(); + ExecPin->PinFriendlyName = Property->GetDisplayNameText(); if (!DelegateSignatureFunction) { diff --git a/Engine/Source/Editor/BlueprintGraph/Private/K2Node_FunctionEntry.cpp b/Engine/Source/Editor/BlueprintGraph/Private/K2Node_FunctionEntry.cpp index 83e16a166488..a86f08278453 100644 --- a/Engine/Source/Editor/BlueprintGraph/Private/K2Node_FunctionEntry.cpp +++ b/Engine/Source/Editor/BlueprintGraph/Private/K2Node_FunctionEntry.cpp @@ -248,6 +248,7 @@ void UK2Node_FunctionEntry::Serialize(FArchive& Ar) if (!LocalVariable.DefaultValue.IsEmpty()) { // If looking for references during save, expand any default values on the local variables + // This is only reliable when saving in the editor, the cook case is handled below if (Ar.IsObjectReferenceCollector() && LocalVariable.VarType.PinCategory == UEdGraphSchema_K2::PC_Struct && LocalVariable.VarType.PinSubCategoryObject.IsValid()) { UScriptStruct* Struct = Cast(LocalVariable.VarType.PinSubCategoryObject.Get()); @@ -335,6 +336,24 @@ void UK2Node_FunctionEntry::Serialize(FArchive& Ar) } } } + + // In editor, fixup soft object ptrs on load on to handle redirects and finding refs for cooking + // We're not handling soft object ptrs inside FStructs because it's a rare edge case and would be a performance hit on load + if (GIsEditor) + { + FSoftObjectPathSerializationScope SetPackage(GetOutermost()->GetFName(), NAME_None, ESoftObjectPathCollectType::AlwaysCollect, ESoftObjectPathSerializeType::SkipSerializeIfArchiveHasSize); + + for (FBPVariableDescription& LocalVariable : LocalVariables) + { + if (!LocalVariable.DefaultValue.IsEmpty() && (LocalVariable.VarType.PinCategory == UEdGraphSchema_K2::PC_SoftObject || LocalVariable.VarType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)) + { + FSoftObjectPath TempRef(LocalVariable.DefaultValue); + TempRef.PostLoadPath(); + TempRef.PreSavePath(); + LocalVariable.DefaultValue = TempRef.ToString(); + } + } + } } } diff --git a/Engine/Source/Editor/Cascade/Private/CascadeEmitterCanvasClient.cpp b/Engine/Source/Editor/Cascade/Private/CascadeEmitterCanvasClient.cpp index 53967ac620de..511a09a290ff 100644 --- a/Engine/Source/Editor/Cascade/Private/CascadeEmitterCanvasClient.cpp +++ b/Engine/Source/Editor/Cascade/Private/CascadeEmitterCanvasClient.cpp @@ -1480,7 +1480,6 @@ void FCascadeEmitterCanvasClient::DrawDraggedModule(UParticleModule* Module, FVi int32 TargetIndex = INDEX_NONE; FindDesiredModulePosition(MousePos, TargetEmitter, TargetIndex); - MousePos += Origin2D; // When dragging, draw the module under the mouse cursor. FVector Translate = FVector(MousePos.X +MouseHoldOffset.X, MousePos.Y+ MouseHoldOffset.Y, 0)/GetDPIScale(); diff --git a/Engine/Source/Editor/ClothPainter/Private/ClothPainter.cpp b/Engine/Source/Editor/ClothPainter/Private/ClothPainter.cpp index 1c73f1d2ac03..0f3538fc87e3 100644 --- a/Engine/Source/Editor/ClothPainter/Private/ClothPainter.cpp +++ b/Engine/Source/Editor/ClothPainter/Private/ClothPainter.cpp @@ -67,7 +67,7 @@ void FClothPainter::Init() Widget = SNew(SClothPaintWidget, this); } -bool FClothPainter::PaintInternal(const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, EMeshPaintAction PaintAction, float PaintStrength) +bool FClothPainter::PaintInternal(const FVector& InCameraOrigin, const TArrayView>& Rays, EMeshPaintAction PaintAction, float PaintStrength) { bool bApplied = false; @@ -75,12 +75,16 @@ bool FClothPainter::PaintInternal(const FVector& InCameraOrigin, const FVector& { USkeletalMesh* SkelMesh = SkeletalMeshComponent->SkeletalMesh; - const FHitResult& HitResult = GetHitResult(InRayOrigin, InRayDirection); - - if (HitResult.bBlockingHit) + for (const TPair& Ray : Rays) { - // Generic per-vertex painting operations - if(!IsPainting()) + const FVector& InRayOrigin = Ray.Key; + const FVector& InRayDirection = Ray.Value; + const FHitResult& HitResult = GetHitResult(InRayOrigin, InRayDirection); + + if (HitResult.bBlockingHit) + { + // Generic per-vertex painting operations + if (!IsPainting()) { BeginTransaction(LOCTEXT("MeshPaint", "Painting Cloth Property Values")); bArePainting = true; @@ -89,21 +93,22 @@ bool FClothPainter::PaintInternal(const FVector& InCameraOrigin, const FVector& const FMeshPaintParameters Parameters = CreatePaintParameters(HitResult, InCameraOrigin, InRayOrigin, InRayDirection, PaintStrength); - FPerVertexPaintActionArgs Args; - Args.Adapter = Adapter.Get(); - Args.CameraPosition = InCameraOrigin; - Args.HitResult = HitResult; - Args.BrushSettings = GetBrushSettings(); - Args.Action = PaintAction; + FPerVertexPaintActionArgs Args; + Args.Adapter = Adapter.Get(); + Args.CameraPosition = InCameraOrigin; + Args.HitResult = HitResult; + Args.BrushSettings = GetBrushSettings(); + Args.Action = PaintAction; - if(SelectedTool->IsPerVertex()) - { - bApplied = MeshPaintHelpers::ApplyPerVertexPaintAction(Args, GetPaintAction(Parameters)); - } - else - { - bApplied = true; - GetPaintAction(Parameters).ExecuteIfBound(Args, INDEX_NONE); + if (SelectedTool->IsPerVertex()) + { + bApplied |= MeshPaintHelpers::ApplyPerVertexPaintAction(Args, GetPaintAction(Parameters)); + } + else + { + bApplied = true; + GetPaintAction(Parameters).ExecuteIfBound(Args, INDEX_NONE); + } } } } @@ -214,10 +219,12 @@ void FClothPainter::RecalculateAutoViewRange() { if(PainterSettings->bAutoViewRange && CurrentMask) { - CurrentMask->CalcRanges(); + float MinValue = MAX_flt; + float MaxValue = -MinValue; + CurrentMask->CalcRanges(MinValue, MaxValue); - PainterSettings->AutoCalculatedViewMin = CurrentMask->MinValue; - PainterSettings->AutoCalculatedViewMax = CurrentMask->MaxValue; + PainterSettings->AutoCalculatedViewMin = MinValue; + PainterSettings->AutoCalculatedViewMax = MaxValue; } else { diff --git a/Engine/Source/Editor/ClothPainter/Private/ClothPainter.h b/Engine/Source/Editor/ClothPainter/Private/ClothPainter.h index af22b4aff353..b7f3534d67d3 100644 --- a/Engine/Source/Editor/ClothPainter/Private/ClothPainter.h +++ b/Engine/Source/Editor/ClothPainter/Private/ClothPainter.h @@ -26,7 +26,7 @@ public: protected: - virtual bool PaintInternal(const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, EMeshPaintAction PaintAction, float PaintStrength) override; + virtual bool PaintInternal(const FVector& InCameraOrigin, const TArrayView>& Rays, EMeshPaintAction PaintAction, float PaintStrength) override; public: diff --git a/Engine/Source/Editor/ClothPainter/Private/SClothAssetSelector.cpp b/Engine/Source/Editor/ClothPainter/Private/SClothAssetSelector.cpp index d90990e0eaa6..d4590814db0a 100644 --- a/Engine/Source/Editor/ClothPainter/Private/SClothAssetSelector.cpp +++ b/Engine/Source/Editor/ClothPainter/Private/SClothAssetSelector.cpp @@ -1126,7 +1126,6 @@ FReply SClothAssetSelector::AddNewMask() NewMask.MaskName = TEXT("New Mask"); NewMask.CurrentTarget = MaskTarget_PhysMesh::None; - NewMask.MaxValue = 0.0f; NewMask.Values.AddZeroed(NumRequiredValues); OnRefresh(); diff --git a/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.cpp b/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.cpp index c72f48077986..b20c78ca7f53 100644 --- a/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.cpp +++ b/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.cpp @@ -197,20 +197,104 @@ bool FAssetContextMenu::AddImportedAssetMenuOptions(FMenuBuilder& MenuBuilder) if (AreImportedAssetActionsVisible()) { TArray ResolvedFilePaths; - GetSelectedAssetSourceFilePaths(ResolvedFilePaths); + TArray SourceFileLabels; + int32 ValidSelectedAssetCount = 0; + GetSelectedAssetSourceFilePaths(ResolvedFilePaths, SourceFileLabels, ValidSelectedAssetCount); MenuBuilder.BeginSection("ImportedAssetActions", LOCTEXT("ImportedAssetActionsMenuHeading", "Imported Asset")); { - // Reimport - MenuBuilder.AddMenuEntry( - LOCTEXT("Reimport", "Reimport"), - LOCTEXT("ReimportTooltip", "Reimport the selected asset(s) from the source file on disk."), - FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), - FUIAction( - FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteReimport), - FCanExecuteAction() + auto CreateSubMenu = [this](FMenuBuilder& SubMenuBuilder, bool bReimportWithNewFile) + { + //Get the data, we cannot use the closure since the lambda will be call when the function scope will be gone + TArray ResolvedFilePaths; + TArray SourceFileLabels; + int32 ValidSelectedAssetCount = 0; + GetSelectedAssetSourceFilePaths(ResolvedFilePaths, SourceFileLabels, ValidSelectedAssetCount); + if (SourceFileLabels.Num() > 0 ) + { + for (int32 SourceFileIndex = 0; SourceFileIndex < SourceFileLabels.Num(); ++SourceFileIndex) + { + FText ReimportLabel = FText::Format(LOCTEXT("ReimportNoLabel", "SourceFile {0}"), SourceFileIndex); + FText ReimportLabelTooltip; + if (ValidSelectedAssetCount == 1) + { + ReimportLabelTooltip = FText::Format(LOCTEXT("ReimportNoLabelTooltip", "Reimport File: {0}"), FText::FromString(ResolvedFilePaths[SourceFileIndex])); + } + if (SourceFileLabels[SourceFileIndex].Len() > 0) + { + ReimportLabel = FText::Format(LOCTEXT("ReimportLabel", "{0}"), FText::FromString(SourceFileLabels[SourceFileIndex])); + if (ValidSelectedAssetCount == 1) + { + ReimportLabelTooltip = FText::Format(LOCTEXT("ReimportLabelTooltip", "Reimport {0} File: {1}"), FText::FromString(SourceFileLabels[SourceFileIndex]), FText::FromString(ResolvedFilePaths[SourceFileIndex])); + } + } + if (bReimportWithNewFile) + { + SubMenuBuilder.AddMenuEntry( + ReimportLabel, + ReimportLabelTooltip, + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteReimportWithNewFile, SourceFileIndex), + FCanExecuteAction() + ) + ); + } + else + { + SubMenuBuilder.AddMenuEntry( + ReimportLabel, + ReimportLabelTooltip, + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteReimport, SourceFileIndex), + FCanExecuteAction() + ) + ); + } + + } + } + }; + + //Reimport Menu + if (ValidSelectedAssetCount == 1 && SourceFileLabels.Num() > 1) + { + MenuBuilder.AddSubMenu( + LOCTEXT("Reimport", "Reimport"), + LOCTEXT("ReimportEmptyTooltip", ""), + FNewMenuDelegate::CreateLambda(CreateSubMenu, false) ); + //With new file + MenuBuilder.AddSubMenu( + LOCTEXT("ReimportWithNewFile", "Reimport With New File"), + LOCTEXT("ReimportEmptyTooltip", ""), + FNewMenuDelegate::CreateLambda(CreateSubMenu, true)); + } + else + { + MenuBuilder.AddMenuEntry( + LOCTEXT("Reimport", "Reimport"), + LOCTEXT("ReimportTooltip", "Reimport the selected asset(s) from the source file on disk."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteReimport, (int32)INDEX_NONE), + FCanExecuteAction() ) ); + if (ValidSelectedAssetCount == 1) + { + //With new file + MenuBuilder.AddMenuEntry( + LOCTEXT("ReimportWithNewFile", "Reimport With New File"), + LOCTEXT("ReimportWithNewFileTooltip", "Reimport the selected asset from a new source file on disk."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateSP(this, &FAssetContextMenu::ExecuteReimportWithNewFile, (int32)INDEX_NONE), + FCanExecuteAction() + ) + ); + } + } // Show Source In Explorer MenuBuilder.AddMenuEntry( @@ -1483,16 +1567,55 @@ bool FAssetContextMenu::CanExecuteImportedAssetActions(const TArray Res return true; } -void FAssetContextMenu::ExecuteReimport() +void FAssetContextMenu::ExecuteReimport(int32 SourceFileIndex /*= INDEX_NONE*/) { // Reimport all selected assets TArray CopyOfSelectedAssets; for (const FAssetData &SelectedAsset : SelectedAssets) - { + { UObject *Asset = SelectedAsset.GetAsset(); CopyOfSelectedAssets.Add(Asset); } - FReimportManager::Instance()->ValidateAllSourceFileAndReimport(CopyOfSelectedAssets); + FReimportManager::Instance()->ValidateAllSourceFileAndReimport(CopyOfSelectedAssets, true, SourceFileIndex, false); +} + +void FAssetContextMenu::ExecuteReimportWithNewFile(int32 SourceFileIndex /*= INDEX_NONE*/) +{ + // Ask for a new files and reimport the selected asset + check(SelectedAssets.Num() == 1); + + TArray CopyOfSelectedAssets; + for (const FAssetData &SelectedAsset : SelectedAssets) + { + UObject *Asset = SelectedAsset.GetAsset(); + CopyOfSelectedAssets.Add(Asset); + } + + TArray AssetSourcePaths; + UClass* ObjectClass = CopyOfSelectedAssets[0]->GetClass(); + FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); + const auto AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(ObjectClass); + if (AssetTypeActions.IsValid()) + { + AssetTypeActions.Pin()->GetResolvedSourceFilePaths(CopyOfSelectedAssets, AssetSourcePaths); + } + + int32 SourceFileIndexToReplace = SourceFileIndex; + //Check if the data is valid + if (SourceFileIndex == INDEX_NONE) + { + check(AssetSourcePaths.Num() <= 1); + //Ask for a new file for the index 0 + SourceFileIndexToReplace = 0; + } + else + { + check(AssetSourcePaths.IsValidIndex(SourceFileIndex)); + } + check(SourceFileIndexToReplace >= 0); + + + FReimportManager::Instance()->ValidateAllSourceFileAndReimport(CopyOfSelectedAssets, true, SourceFileIndexToReplace, true); } void FAssetContextMenu::ExecuteFindSourceInExplorer(const TArray ResolvedFilePaths) @@ -1530,14 +1653,14 @@ void FAssetContextMenu::GetSelectedAssetsByClass(TMap } } -void FAssetContextMenu::GetSelectedAssetSourceFilePaths(TArray& OutFilePaths) const +void FAssetContextMenu::GetSelectedAssetSourceFilePaths(TArray& OutFilePaths, TArray& OutUniqueSourceFileLabels, int32 &OutValidSelectedAssetCount) const { OutFilePaths.Empty(); - + OutUniqueSourceFileLabels.Empty(); TMap > SelectedAssetsByClass; GetSelectedAssetsByClass(SelectedAssetsByClass); FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); - + OutValidSelectedAssetCount = 0; // Get the source file paths for the assets of each type for (const auto& AssetsByClassPair : SelectedAssetsByClass) { @@ -1545,10 +1668,17 @@ void FAssetContextMenu::GetSelectedAssetSourceFilePaths(TArray& OutFile if (AssetTypeActions.IsValid()) { const auto& TypeAssets = AssetsByClassPair.Value; + OutValidSelectedAssetCount += TypeAssets.Num(); TArray AssetSourcePaths; AssetTypeActions.Pin()->GetResolvedSourceFilePaths(TypeAssets, AssetSourcePaths); - OutFilePaths.Append(AssetSourcePaths); + + TArray AssetSourceLabels; + AssetTypeActions.Pin()->GetSourceFileLabels(TypeAssets, AssetSourceLabels); + for (const FString& Label : AssetSourceLabels) + { + OutUniqueSourceFileLabels.AddUnique(Label); + } } } } diff --git a/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.h b/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.h index 3b2e29a75c23..1f5a5eb2bd74 100644 --- a/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.h +++ b/Engine/Source/Editor/ContentBrowser/Private/AssetContextMenu.h @@ -91,7 +91,7 @@ private: void GetSelectedAssetsByClass(TMap >& OutSelectedAssetsByClass) const; /** Helper to collect resolved filepaths for all selected assets */ - void GetSelectedAssetSourceFilePaths(TArray& OutFilePaths) const; + void GetSelectedAssetSourceFilePaths(TArray& OutFilePaths, TArray& OutUniqueSourceFileLabels, int32 &OutValidSelectedAssetCount) const; /** Handler to check to see if a imported asset actions should be visible in the menu */ bool AreImportedAssetActionsVisible() const; @@ -100,7 +100,9 @@ private: bool CanExecuteImportedAssetActions(const TArray ResolvedFilePaths) const; /** Handler for Reimport */ - void ExecuteReimport(); + void ExecuteReimport(int32 SourceFileIndex = INDEX_NONE); + + void ExecuteReimportWithNewFile(int32 SourceFileIndex = INDEX_NONE); /** Handler for FindInExplorer */ void ExecuteFindSourceInExplorer(const TArray ResolvedFilePaths); diff --git a/Engine/Source/Editor/ContentBrowser/Private/AssetViewWidgets.cpp b/Engine/Source/Editor/ContentBrowser/Private/AssetViewWidgets.cpp index b082a7cd6b10..5875e484f024 100644 --- a/Engine/Source/Editor/ContentBrowser/Private/AssetViewWidgets.cpp +++ b/Engine/Source/Editor/ContentBrowser/Private/AssetViewWidgets.cpp @@ -758,7 +758,12 @@ TSharedRef SAssetViewItem::CreateToolTipWidget() const { for (const auto& File : ImportInfo->SourceFiles) { - AddToToolTipInfoBox( InfoBox, LOCTEXT("TileViewTooltipSourceFile", "Source File"), FText::FromString(File.RelativeFilename), false ); + FText SourceLabel = LOCTEXT("TileViewTooltipSourceFile", "Source File"); + if (File.DisplayLabelName.Len() > 0) + { + SourceLabel = FText::FromString(FText(LOCTEXT("TileViewTooltipSourceFile", "Source File")).ToString() + TEXT(" (") + File.DisplayLabelName + TEXT(")")); + } + AddToToolTipInfoBox( InfoBox, SourceLabel, FText::FromString(File.RelativeFilename), false ); } } diff --git a/Engine/Source/Editor/ContentBrowser/Private/FrontendFilters.cpp b/Engine/Source/Editor/ContentBrowser/Private/FrontendFilters.cpp index 9515d4d31f6e..70a7050cf429 100644 --- a/Engine/Source/Editor/ContentBrowser/Private/FrontendFilters.cpp +++ b/Engine/Source/Editor/ContentBrowser/Private/FrontendFilters.cpp @@ -746,7 +746,7 @@ void FFrontendFilter_CheckedOut::SourceControlOperationComplete(const FSourceCon ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TArray CheckedOutFiles = SourceControlProvider.GetCachedStateByPredicate( - [](const FSourceControlStateRef& State) { return State->IsCheckedOut(); } + [](const FSourceControlStateRef& State) { return State->IsCheckedOut() || State->IsAdded(); } ); FString PathPart; @@ -833,7 +833,7 @@ void FFrontendFilter_NotSourceControlled::RequestStatus() // Request the state of files at filter construction time to make sure files have the correct state for the filter TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); - + TArray Filenames; Filenames.Add(FPaths::ConvertRelativePathToFull(FPaths::EngineContentDir())); Filenames.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir())); diff --git a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditor.cpp b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditor.cpp index f9290b93bee4..ea3881fe604d 100644 --- a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditor.cpp +++ b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditor.cpp @@ -409,31 +409,25 @@ void FCurveTableEditor::CacheCurveTableForEditing() RowNameColumnWidth = 10.0f; const UCurveTable* Table = GetCurveTable(); - if (!Table || Table->RowMap.Num() == 0) + if (!Table || Table->GetRowMap().Num() == 0) { AvailableColumns.Empty(); AvailableRows.Empty(); return; } - TArray Names; - TArray Curves; - - // get the row names and curves they represent - Table->RowMap.GenerateKeyArray(Names); - Table->RowMap.GenerateValueArray(Curves); - TSharedRef FontMeasure = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FTextBlockStyle& CellTextStyle = FEditorStyle::GetWidgetStyle("DataTableEditor.CellText"); static const float CellPadding = 10.0f; // Find unique column titles TArray UniqueColumns; - for (int32 CurvesIdx = 0; CurvesIdx < Curves.Num(); ++CurvesIdx) + for (const TPair CurveRow : Table->GetRowMap()) { - for (auto CurveIt(Curves[CurvesIdx]->GetKeyIterator()); CurveIt; ++CurveIt) + FRealCurve* Curve = CurveRow.Value; + for (auto CurveIt(Curve->GetKeyHandleIterator()); CurveIt; ++CurveIt) { - UniqueColumns.AddUnique(CurveIt->Time); + UniqueColumns.AddUnique(Curve->GetKeyTime(*CurveIt)); } } @@ -453,10 +447,11 @@ void FCurveTableEditor::CacheCurveTableForEditing() } // Each curve is a row entry - AvailableRows.Reset(Curves.Num()); - for (int32 CurvesIdx = 0; CurvesIdx < Curves.Num(); ++CurvesIdx) + AvailableRows.Reset(Table->GetRowMap().Num()); + for (const TPair CurveRow : Table->GetRowMap()) { - const FName& CurveName = Names[CurvesIdx]; + const FName& CurveName = CurveRow.Key; + FRealCurve* Curve = CurveRow.Value; FCurveTableEditorRowListViewDataPtr CachedRowData = MakeShareable(new FCurveTableEditorRowListViewData()); CachedRowData->RowId = CurveName; @@ -469,22 +464,23 @@ void FCurveTableEditor::CacheCurveTableForEditing() RowNameColumnWidth = FMath::Max(RowNameColumnWidth, RowNameWidth); CachedRowData->CellData.AddDefaulted(AvailableColumns.Num()); - { - for (auto It(Curves[CurvesIdx]->GetKeyIterator()); It; ++It) - { - int32 ColumnIndex = 0; - if (UniqueColumns.Find(It->Time, ColumnIndex)) - { - FCurveTableEditorColumnHeaderDataPtr CachedColumnData = AvailableColumns[ColumnIndex]; - const FText CellText = FText::AsNumber(It->Value); - CachedRowData->CellData[ColumnIndex] = CellText; + for (auto It(Curve->GetKeyHandleIterator()); It; ++It) + { + const FKeyHandle& KeyHandle = *It; + int32 ColumnIndex = 0; + const TPair TimeValuePair = Curve->GetKeyTimeValuePair(KeyHandle); + if (UniqueColumns.Find(TimeValuePair.Key, ColumnIndex)) + { + FCurveTableEditorColumnHeaderDataPtr CachedColumnData = AvailableColumns[ColumnIndex]; + + const FText CellText = FText::AsNumber(TimeValuePair.Value); + CachedRowData->CellData[ColumnIndex] = CellText; const float CellWidth = FontMeasure->Measure(CellText, CellTextStyle.Font).X + CellPadding; CachedColumnData->DesiredColumnWidth = FMath::Max(CachedColumnData->DesiredColumnWidth, CellWidth); } } - } AvailableRows.Add(CachedRowData); } diff --git a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.cpp b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.cpp index e5d900a33191..d60c34b51133 100644 --- a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.cpp +++ b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.cpp @@ -2,7 +2,7 @@ #include "CurveTableEditorHandle.h" -FRichCurve* FCurveTableEditorHandle::GetCurve() const +FRealCurve* FCurveTableEditorHandle::GetCurve() const { if (CurveTable != nullptr && RowName != NAME_None) { @@ -15,7 +15,7 @@ TArray FCurveTableEditorHandle::GetCurves() const { TArray Curves; - const FRichCurve* Curve = GetCurve(); + const FRealCurve* Curve = GetCurve(); if (Curve) { Curves.Add(FRichCurveEditInfoConst(Curve, RowName)); @@ -28,7 +28,7 @@ TArray FCurveTableEditorHandle::GetCurves() { TArray Curves; - FRichCurve* Curve = GetCurve(); + FRealCurve* Curve = GetCurve(); if (Curve) { Curves.Add(FRichCurveEditInfo(Curve, RowName)); diff --git a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.h b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.h index 8a5d3f247df1..6fa4a5a17ccf 100644 --- a/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.h +++ b/Engine/Source/Editor/CurveTableEditor/Private/CurveTableEditorHandle.h @@ -56,5 +56,5 @@ struct FCurveTableEditorHandle : public FCurveOwnerInterface } /** Get the curve straight from the row handle */ - FRichCurve* GetCurve() const; + FRealCurve* GetCurve() const; }; diff --git a/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.cpp b/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.cpp index 2075a04af518..f746bdd684c7 100644 --- a/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.cpp +++ b/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.cpp @@ -1,27 +1,28 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "DataTableEditor.h" +#include "DataTableEditorModule.h" #include "Dom/JsonObject.h" -#include "Widgets/Text/STextBlock.h" -#include "Misc/FileHelper.h" -#include "Modules/ModuleManager.h" -#include "Serialization/JsonReader.h" -#include "Policies/PrettyJsonPrintPolicy.h" -#include "Serialization/JsonSerializer.h" +#include "Editor.h" +#include "EditorStyleSet.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" -#include "Widgets/Layout/SScrollBar.h" #include "Framework/Layout/Overscroll.h" -#include "Widgets/Layout/SScrollBox.h" -#include "EditorStyleSet.h" -#include "DataTableEditorModule.h" -#include "Editor.h" -#include "Widgets/Input/SSearchBox.h" -#include "Widgets/Docking/SDockTab.h" -#include "Widgets/Views/SListView.h" #include "IDocumentation.h" +#include "Misc/FileHelper.h" +#include "Modules/ModuleManager.h" +#include "Policies/PrettyJsonPrintPolicy.h" +#include "SDataTableListViewRowName.h" +#include "Serialization/JsonReader.h" +#include "Serialization/JsonSerializer.h" +#include "Widgets/Docking/SDockTab.h" +#include "Widgets/Input/SSearchBox.h" +#include "Widgets/Layout/SScrollBar.h" +#include "Widgets/Layout/SScrollBox.h" #include "Widgets/SToolTip.h" - +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Views/SListView.h" + #define LOCTEXT_NAMESPACE "DataTableEditor" const FName FDataTableEditor::DataTableTabId("DataTableEditor_DataTable"); @@ -383,23 +384,9 @@ void FDataTableEditor::SaveLayoutData() TSharedRef FDataTableEditor::MakeRowNameWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const TSharedRef& OwnerTable) { - return - SNew(STableRow, OwnerTable) - .Style(FEditorStyle::Get(), "DataTableEditor.NameListViewRow") - [ - SNew(SBox) - .Padding(FMargin(4, 2, 4, 2)) - [ - SNew(SBox) - .HeightOverride(InRowDataPtr->DesiredRowHeight) - [ - SNew(STextBlock) - .ColorAndOpacity(this, &FDataTableEditor::GetRowTextColor, InRowDataPtr->RowId) - .Text(InRowDataPtr->DisplayName) - .HighlightText(this, &FDataTableEditor::GetFilterText) - ] - ] - ]; + return SNew(SDataTableListViewRowName, OwnerTable) + .DataTableEditor(SharedThis(this)) + .RowDataPtr(InRowDataPtr); } TSharedRef FDataTableEditor::MakeRowWidget(FDataTableEditorRowListViewDataPtr InRowDataPtr, const TSharedRef& OwnerTable) diff --git a/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.h b/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.h index d6119fbf702b..ab1790d16acb 100644 --- a/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.h +++ b/Engine/Source/Editor/DataTableEditor/Private/DataTableEditor.h @@ -75,6 +75,10 @@ public: void SetHighlightedRow(FName Name); + FText GetFilterText() const; + + FSlateColor GetRowTextColor(FName RowName) const; + protected: void RefreshCachedDataTable(const FName InCachedSelection = NAME_None, const bool bUpdateEvenIfValid = false); @@ -82,12 +86,9 @@ protected: void UpdateVisibleRows(const FName InCachedSelection = NAME_None, const bool bUpdateEvenIfValid = false); void RestoreCachedSelection(const FName InCachedSelection, const bool bUpdateEvenIfValid = false); - - FText GetFilterText() const; - + void OnFilterTextChanged(const FText& InFilterText); - FSlateColor GetRowTextColor(FName RowName) const; FText GetCellText(FDataTableEditorRowListViewDataPtr InRowDataPointer, int32 ColumnIndex) const; FText GetCellToolTipText(FDataTableEditorRowListViewDataPtr InRowDataPointer, int32 ColumnIndex) const; diff --git a/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.cpp b/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.cpp new file mode 100644 index 000000000000..842062498687 --- /dev/null +++ b/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.cpp @@ -0,0 +1,64 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#include "SDataTableListViewRowName.h" + +#include "AssetData.h" +#include "DataTableEditor.h" +#include "DataTableRowUtlis.h" +#include "EditorStyleSet.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" + +void SDataTableListViewRowName::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) +{ + RowDataPtr = InArgs._RowDataPtr; + DataTableEditor = InArgs._DataTableEditor; + STableRow::Construct( + typename STableRow::FArguments() + .Style(FEditorStyle::Get(), "DataTableEditor.NameListViewRow") + .Content() + [ + SNew(SBox) + .Padding(FMargin(4, 2, 4, 2)) + [ + SNew(SBox) + .HeightOverride(RowDataPtr->DesiredRowHeight) + [ + SNew(STextBlock) + .ColorAndOpacity(DataTableEditor.Get(), &FDataTableEditor::GetRowTextColor, RowDataPtr->RowId) + .Text(RowDataPtr->DisplayName) + .HighlightText(DataTableEditor.Get(), &FDataTableEditor::GetFilterText) + ] + ] + ], + InOwnerTableView + ); +} + +FReply SDataTableListViewRowName::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && RowDataPtr.IsValid() && FEditorDelegates::OnOpenReferenceViewer.IsBound()) + { + TSharedRef MenuWidget = FDataTableRowUtils::MakeRowActionsMenu(FExecuteAction::CreateSP(this, &SDataTableListViewRowName::OnSearchForReferences)); + + FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); + + FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, MenuWidget, MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect::ContextMenu); + + return FReply::Handled(); + } + + return STableRow::OnMouseButtonUp(MyGeometry, MouseEvent); +} + +void SDataTableListViewRowName::OnSearchForReferences() +{ + if (DataTableEditor.IsValid() && RowDataPtr.IsValid()) + { + UDataTable* SourceDataTable = const_cast(DataTableEditor->GetDataTable()); + + TArray AssetIdentifiers; + AssetIdentifiers.Add(FAssetIdentifier(SourceDataTable, RowDataPtr->RowId)); + + FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers); + } +} diff --git a/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.h b/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.h new file mode 100644 index 000000000000..f890d0e4e96d --- /dev/null +++ b/Engine/Source/Editor/DataTableEditor/Private/SDataTableListViewRowName.h @@ -0,0 +1,39 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "InputCoreTypes.h" +#include "Input/Reply.h" +#include "Widgets/Views/STableRow.h" +#include "Framework/Commands/UIAction.h" +#include "Framework/Application/MenuStack.h" +#include "Framework/Application/SlateApplication.h" +#include "DataTableEditorUtils.h" + +class FDataTableEditor; + +/** + * A widget to represent a row in a Data Table Editor widget. This widget allows us to do things like right-click + * and take actions on a particular row of a Data Table. + */ +class SDataTableListViewRowName : public STableRow +{ +public: + + SLATE_BEGIN_ARGS(SDataTableListViewRowName){} + /** The owning object. This allows us access to the actual data table being edited as well as some other API functions. */ + SLATE_ARGUMENT(TSharedPtr, DataTableEditor) + /** The row we're working with to allow us to get naming information. */ + SLATE_ARGUMENT(FDataTableEditorRowListViewDataPtr, RowDataPtr) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView); + + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; +private: + + void OnSearchForReferences(); + FDataTableEditorRowListViewDataPtr RowDataPtr; + TSharedPtr DataTableEditor; +}; \ No newline at end of file diff --git a/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.cpp b/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.cpp new file mode 100644 index 000000000000..6fe23b1290cf --- /dev/null +++ b/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.cpp @@ -0,0 +1,37 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#include "DataTableRowUtlis.h" +#include "DetailWidgetRow.h" +#include "Editor.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Textures/SlateIcon.h" +#include "Widgets/SWidget.h" + +#define LOCTEXT_NAMESPACE "FDataTableRowUtils" + +const FText FDataTableRowUtils::SearchForReferencesActionName = LOCTEXT("FDataTableRowUtils_SearchForReferences", "Find Row References"); +const FText FDataTableRowUtils::SearchForReferencesActionTooltip = LOCTEXT("FDataTableRowUtils_SearchForReferencesTooltip", "Find assets that reference this Row"); + +TSharedRef FDataTableRowUtils::MakeRowActionsMenu(FExecuteAction SearchForReferencesAction) +{ + if (SearchForReferencesAction.IsBound()) + { + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.AddMenuEntry(SearchForReferencesActionName, SearchForReferencesActionTooltip, + FSlateIcon(), FUIAction(SearchForReferencesAction)); + return MenuBuilder.MakeWidget(); + } + + return SNullWidget::NullWidget; +} + +void FDataTableRowUtils::AddSearchForReferencesContextMenu(FDetailWidgetRow& RowNameDetailWidget, FExecuteAction SearchForReferencesAction) +{ + if (SearchForReferencesAction.IsBound() && FEditorDelegates::OnOpenReferenceViewer.IsBound()) + { + RowNameDetailWidget.AddCustomContextMenuAction(FUIAction(SearchForReferencesAction), SearchForReferencesActionName, + SearchForReferencesActionTooltip, FSlateIcon()); + } +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.h b/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.h new file mode 100644 index 000000000000..89da50fc3b25 --- /dev/null +++ b/Engine/Source/Editor/DataTableEditor/Public/DataTableRowUtlis.h @@ -0,0 +1,22 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "Framework/Commands/UIAction.h" + +class SWidget; +class FDetailWidgetRow; + +class DATATABLEEDITOR_API FDataTableRowUtils +{ +public: + static TSharedRef MakeRowActionsMenu(FExecuteAction SearchForReferencesAction); + static void AddSearchForReferencesContextMenu(FDetailWidgetRow& RowNameDetailWidget, FExecuteAction SearchForReferencesAction); + +private: + static const FText SearchForReferencesActionName; + static const FText SearchForReferencesActionTooltip; +}; + diff --git a/Engine/Source/Editor/DetailCustomizations/DetailCustomizations.Build.cs b/Engine/Source/Editor/DetailCustomizations/DetailCustomizations.Build.cs index 8f0d1762b0be..94dd6349ed69 100644 --- a/Engine/Source/Editor/DetailCustomizations/DetailCustomizations.Build.cs +++ b/Engine/Source/Editor/DetailCustomizations/DetailCustomizations.Build.cs @@ -58,6 +58,7 @@ public class DetailCustomizations : ModuleRules "AdvancedPreviewScene", "AudioSettingsEditor", "HeadMountedDisplay", + "DataTableEditor", } ); diff --git a/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.cpp b/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.cpp index 1dcbbddba013..41c782047593 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.cpp +++ b/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.cpp @@ -15,6 +15,8 @@ #include "DetailWidgetRow.h" #include "DetailLayoutBuilder.h" +#include "ScopedTransaction.h" + #define LOCTEXT_NAMESPACE "AssetImportDataCustomization" TSharedRef FAssetImportDataCustomization::MakeInstance() @@ -43,11 +45,17 @@ void FAssetImportDataCustomization::CustomizeChildren( TSharedRefSourceFiles.IsValidIndex(Index) && Info->SourceFiles[Index].DisplayLabelName.Len() > 0) + { + SourceFileLabel = FText::FromString(SourceFileText.ToString() + TEXT(" (") + Info->SourceFiles[Index].DisplayLabelName + TEXT(")")); + } + + ChildBuilder.AddCustomRow(SourceFileLabel) .NameContent() [ SNew(STextBlock) - .Text(SourceFileText) + .Text(SourceFileLabel) .Font(Font) ] .ValueContent() @@ -97,11 +105,53 @@ void FAssetImportDataCustomization::CustomizeChildren( TSharedRef()) [ - SNew(SEditableText) - .IsReadOnly(true) - .Text(this, &FAssetImportDataCustomization::GetTimestampText, Index) - .Font(Font) + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(1.0f) + [ + SNew(SEditableText) + .IsReadOnly(true) + .Text(this, &FAssetImportDataCustomization::GetTimestampText, Index) + .Font(Font) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .IsEnabled(this, &FAssetImportDataCustomization::IsPropagateFromAbovePathEnable, Index) + .OnClicked(this, &FAssetImportDataCustomization::OnPropagateFromAbovePathClicked, Index) + .ToolTipText(LOCTEXT("PropagateFromAbovePath_Tooltip", "Use the above source path to set this path.")) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush("ArrowDown")) + ] + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .AutoWidth() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .IsEnabled(this, &FAssetImportDataCustomization::IsPropagateFromBelowPathEnable, Index) + .OnClicked(this, &FAssetImportDataCustomization::OnPropagateFromBelowPathClicked, Index) + .ToolTipText(LOCTEXT("PropagateFromBelowPath_Tooltip", "Use the below source path to set this path.")) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush("ArrowUp")) + ] + ] ]; } } @@ -160,6 +210,37 @@ UAssetImportData* FAssetImportDataCustomization::GetOuterClass() const return OuterObjects.Num() ? Cast(OuterObjects[0]) : nullptr; } +class FImportDataSourceFileTransactionScope +{ +public: + FImportDataSourceFileTransactionScope(FText TransactionName, UAssetImportData* InImportData) + { + check(InImportData); + ImportData = InImportData; + FScopedTransaction Transaction(TransactionName); + + bIsTransactionnal = (ImportData->GetFlags() & RF_Transactional) > 0; + if (!bIsTransactionnal) + { + ImportData->SetFlags(RF_Transactional); + } + + ImportData->Modify(); + } + + ~FImportDataSourceFileTransactionScope() + { + if (!bIsTransactionnal) + { + ImportData->ClearFlags(RF_Transactional); + } + ImportData->MarkPackageDirty(); + } +private: + bool bIsTransactionnal; + UAssetImportData* ImportData; +}; + FReply FAssetImportDataCustomization::OnChangePathClicked(int32 Index) const { UAssetImportData* ImportData = GetOuterClass(); @@ -176,6 +257,7 @@ FReply FAssetImportDataCustomization::OnChangePathClicked(int32 Index) const FReimportManager::Instance()->GetNewReimportPath(Obj, OpenFilenames); if (OpenFilenames.Num() == 1) { + FImportDataSourceFileTransactionScope TransactionScope(LOCTEXT("SourceReimportChangePath", "Change source file path"), ImportData); if (!Info || !Info->SourceFiles.IsValidIndex(Index)) { ImportData->UpdateFilenameOnly(FPaths::ConvertRelativePathToFull(OpenFilenames[0])); @@ -184,7 +266,6 @@ FReply FAssetImportDataCustomization::OnChangePathClicked(int32 Index) const { ImportData->UpdateFilenameOnly(FPaths::ConvertRelativePathToFull(OpenFilenames[0]), Index); } - ImportData->MarkPackageDirty(); } return FReply::Handled(); } @@ -194,12 +275,48 @@ FReply FAssetImportDataCustomization::OnClearPathClicked(int32 Index) const UAssetImportData* ImportData = GetOuterClass(); if (ImportData && ImportData->SourceData.SourceFiles.IsValidIndex(Index)) { + FImportDataSourceFileTransactionScope TransactionScope(LOCTEXT("SourceReimportClearPath", "Clear Source file path"), ImportData); ImportData->SourceData.SourceFiles[Index] = FAssetImportInfo::FSourceFile(FString()); - ImportData->MarkPackageDirty(); } return FReply::Handled(); } +bool FAssetImportDataCustomization::IsPropagateFromAbovePathEnable(int32 Index) const +{ + UAssetImportData* ImportData = GetOuterClass(); + return (ImportData && ImportData->SourceData.SourceFiles.IsValidIndex(Index) && ImportData->SourceData.SourceFiles.IsValidIndex(Index - 1)); +} + +bool FAssetImportDataCustomization::IsPropagateFromBelowPathEnable(int32 Index) const +{ + UAssetImportData* ImportData = GetOuterClass(); + return (ImportData && ImportData->SourceData.SourceFiles.IsValidIndex(Index) && ImportData->SourceData.SourceFiles.IsValidIndex(Index + 1)); +} + +void FAssetImportDataCustomization::PropagatePath(int32 SrcIndex, int32 DstIndex) const +{ + UAssetImportData* ImportData = GetOuterClass(); + if (ImportData && ImportData->SourceData.SourceFiles.IsValidIndex(DstIndex) && ImportData->SourceData.SourceFiles.IsValidIndex(SrcIndex)) + { + FImportDataSourceFileTransactionScope TransactionScope(LOCTEXT("SourceReimportPropagateFromAbove", "Propagate source file path"), ImportData); + FString OriginalLabel = ImportData->SourceData.SourceFiles[DstIndex].DisplayLabelName; + ImportData->SourceData.SourceFiles[DstIndex] = ImportData->SourceData.SourceFiles[SrcIndex]; + ImportData->SourceData.SourceFiles[DstIndex].DisplayLabelName = OriginalLabel; + } +} + +FReply FAssetImportDataCustomization::OnPropagateFromAbovePathClicked(int32 Index) const +{ + PropagatePath(Index - 1, Index); + return FReply::Handled(); +} + + +FReply FAssetImportDataCustomization::OnPropagateFromBelowPathClicked(int32 Index) const +{ + PropagatePath(Index + 1, Index); + return FReply::Handled(); +} #undef LOCTEXT_NAMESPACE diff --git a/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.h b/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.h index 33625869d76b..1451b109cfd2 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.h +++ b/Engine/Source/Editor/DetailCustomizations/Private/AssetImportDataCustomization.h @@ -30,6 +30,14 @@ private: /** Handle the user requesting that the specified index be cleared */ FReply OnClearPathClicked(int32 Index) const; + /** Handle the user requesting that the Path use at Index - 1 will be replace the path at Index */ + FReply OnPropagateFromAbovePathClicked(int32 Index) const; + bool IsPropagateFromAbovePathEnable(int32 Index) const; + + /** Handle the user requesting that the Path use at Index + 1 will be replace the path at Index */ + FReply OnPropagateFromBelowPathClicked(int32 Index) const; + bool IsPropagateFromBelowPathEnable(int32 Index) const; + /** Access the struct we are editing */ FAssetImportInfo* GetEditStruct() const; @@ -42,6 +50,7 @@ private: private: + void PropagatePath(int32 SrcIndex, int32 DstIndex) const; /** Property handle of the property we're editing */ TSharedPtr PropertyHandle; }; diff --git a/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.cpp b/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.cpp index 57d05e5516f3..5a2a959382dc 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.cpp +++ b/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.cpp @@ -1,7 +1,10 @@ // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "DataTableCustomization.h" +#include "DataTableRowUtlis.h" +#include "Editor.h" #include "Framework/Application/SlateApplication.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SSearchBox.h" #define LOCTEXT_NAMESPACE "FDataTableCustomizationLayout" @@ -55,11 +58,16 @@ void FDataTableCustomizationLayout::CustomizeHeader(TSharedRefSetOnPropertyValueChanged(OnDataTableChangedDelegate); + HeaderRow .NameContent() [ InStructPropertyHandle->CreatePropertyNameWidget(FText::GetEmpty(), FText::GetEmpty(), false) ]; + + FDataTableRowUtils::AddSearchForReferencesContextMenu(HeaderRow, FExecuteAction::CreateSP(this, &FDataTableCustomizationLayout::OnSearchForReferences)); } void FDataTableCustomizationLayout::CustomizeChildren(TSharedRef InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) @@ -124,6 +132,21 @@ void FDataTableCustomizationLayout::HandleMenuOpen() FSlateApplication::Get().SetKeyboardFocus(SearchBox); } +void FDataTableCustomizationLayout::OnSearchForReferences() +{ + if (CurrentSelectedItem.IsValid() && !CurrentSelectedItem->IsEmpty() && DataTablePropertyHandle.IsValid() && DataTablePropertyHandle->IsValidHandle()) + { + UObject* SourceDataTable; + DataTablePropertyHandle->GetValue(SourceDataTable); + FName RowName(**CurrentSelectedItem); + + TArray AssetIdentifiers; + AssetIdentifiers.Add(FAssetIdentifier(SourceDataTable, RowName)); + + FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers); + } +} + TSharedRef FDataTableCustomizationLayout::GetListContent() { SAssignNew(RowNameComboListView, SListView >) @@ -182,20 +205,18 @@ FText FDataTableCustomizationLayout::GetRowNameComboBoxContentText() const { FString RowNameValue; const FPropertyAccess::Result RowResult = RowNamePropertyHandle->GetValue(RowNameValue); - if (RowResult != FPropertyAccess::MultipleValues) + if (RowResult == FPropertyAccess::Success) { - TSharedPtr SelectedRowName = CurrentSelectedItem; - if (SelectedRowName.IsValid()) - { - return FText::FromString(*SelectedRowName); - } - else - { - return LOCTEXT("DataTable_None", "None"); - } + return FText::FromString(*RowNameValue); + } + else if (RowResult == FPropertyAccess::Fail) + { + return LOCTEXT("DataTable_None", "None"); + } + else + { + return LOCTEXT("MultipleValues", "Multiple Values"); } - - return LOCTEXT("MultipleValues", "Multiple Values"); } void FDataTableCustomizationLayout::OnSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectInfo) diff --git a/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.h b/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.h index be6411d391da..806b3c00b65b 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.h +++ b/Engine/Source/Editor/DetailCustomizations/Private/DataTableCustomization.h @@ -64,6 +64,8 @@ private: void HandleMenuOpen(); + void OnSearchForReferences(); + /** The comboButton objects */ TSharedPtr RowNameComboButton; TSharedPtr SearchBox; diff --git a/Engine/Source/Editor/DetailCustomizations/Private/FbxImportUIDetails.cpp b/Engine/Source/Editor/DetailCustomizations/Private/FbxImportUIDetails.cpp index 2650503f1237..56a998eabb51 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/FbxImportUIDetails.cpp +++ b/Engine/Source/Editor/DetailCustomizations/Private/FbxImportUIDetails.cpp @@ -114,28 +114,32 @@ bool SkipImportProperty(TSharedPtr Handle, const FString &MetaD bool FFbxImportUIDetails::ShowCompareResult() { bool bHasMaterialConflict = false; - bool bHasSkeletonConflict = false; + ImportCompareHelper::ECompareResult SkeletonCompareResult = ImportCompareHelper::ECompareResult::SCR_None; bool bShowCompareResult = ImportUI->bIsReimport && ImportUI->ReimportMesh != nullptr && ImportUI->OnUpdateCompareFbx.IsBound(); if (bShowCompareResult) { //Always update the compare data with the current option ImportUI->OnUpdateCompareFbx.Execute(); bHasMaterialConflict = ImportUI->MaterialCompareData.HasConflict(); - bHasSkeletonConflict = ImportUI->SkeletonCompareData.HasConflict(); - if (bHasMaterialConflict || bHasSkeletonConflict) + SkeletonCompareResult = ImportUI->SkeletonCompareData.CompareResult; + if (bHasMaterialConflict || SkeletonCompareResult != ImportCompareHelper::ECompareResult::SCR_None) { FName ConflictCategoryName = TEXT("Conflicts"); IDetailCategoryBuilder& CategoryBuilder = CachedDetailBuilder->EditCategory(ConflictCategoryName, LOCTEXT("CategoryConflictsName", "Conflicts"), ECategoryPriority::Important); - auto BuildConflictRow = [this, &CategoryBuilder](const FText& CategoryName, const FText& Conflict_NameContent, const FText& Conflict_ButtonTooltip, const FText& Conflict_ButtonText, ConflictDialogType DialogType) + auto BuildConflictRow = [this, &CategoryBuilder](const FText& CategoryName, const FText& Conflict_NameContent, const FText& Conflict_ButtonTooltip, const FText& Conflict_ButtonText, ConflictDialogType DialogType, const FSlateBrush* Brush, const FText& Conflict_IconTooltip) { CategoryBuilder.AddCustomRow(CategoryName).WholeRowContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) .AutoWidth() + .Padding(2.0f, 2.0f, 5.0f, 2.0f) [ SNew(SImage) - .Image(FEditorStyle::GetBrush("Icons.Error")) + .ToolTipText(Conflict_IconTooltip) + .Image(Brush) ] + SHorizontalBox::Slot() .FillWidth(1.0f) @@ -168,16 +172,39 @@ bool FFbxImportUIDetails::ShowCompareResult() LOCTEXT("MaterialConflict_NameContent", "Unmatched Materials"), LOCTEXT("MaterialConflict_ButtonShowTooltip", "Show a detailed view of the materials conflict."), LOCTEXT("MaterialConflict_ButtonShow", "Show Conflict"), - ConflictDialogType::Conflict_Material); + ConflictDialogType::Conflict_Material, + FEditorStyle::GetBrush("Icons.Error"), + LOCTEXT("MaterialConflict_IconTooltip", "There is one or more material(s) that do not match.") + ); } - if (bHasSkeletonConflict) + if (SkeletonCompareResult != ImportCompareHelper::ECompareResult::SCR_None) { + FText IconTooltip; + if ((SkeletonCompareResult & ImportCompareHelper::ECompareResult::SCR_SkeletonBadRoot) > ImportCompareHelper::ECompareResult::SCR_None) + { + IconTooltip = (LOCTEXT("SkeletonConflictBadRoot_IconTooltip", "(Error) Root bone: The root bone of the incoming fbx do not match the root bone of the current skeletalmesh asset. Import will probably fail!")); + } + else if ((SkeletonCompareResult & ImportCompareHelper::ECompareResult::SCR_SkeletonMissingBone) > ImportCompareHelper::ECompareResult::SCR_None) + { + IconTooltip = (LOCTEXT("SkeletonConflictBadRoot_IconTooltip", "(Warning) Deleted bones: Some bones of the of the current skeletalmesh asset are not use by the incoming fbx.")); + } + else + { + IconTooltip = (LOCTEXT("SkeletonConflictBadRoot_IconTooltip", "(Info) Added bones: Some bones in the incoming fbx do not exist in the current skeletalmesh asset.")); + } + + const FSlateBrush* Brush = (SkeletonCompareResult & ImportCompareHelper::ECompareResult::SCR_SkeletonBadRoot) > ImportCompareHelper::ECompareResult::SCR_None ? FEditorStyle::GetBrush("Icons.Error") + : (SkeletonCompareResult & ImportCompareHelper::ECompareResult::SCR_SkeletonMissingBone) > ImportCompareHelper::ECompareResult::SCR_None ? FEditorStyle::GetBrush("Icons.Warning") + : FEditorStyle::GetBrush("Icons.Info"); + BuildConflictRow(LOCTEXT("SkeletonConflict_RowFilter", "Skeleton conflict"), LOCTEXT("SkeletonConflict_NameContent", "Unmatched Skeleton joints"), LOCTEXT("SkeletonConflict_ButtonShowTooltip", "Show a detailed view of the skeleton joints conflict."), LOCTEXT("SkeletonConflict_ButtonShow", "Show Conflict"), - ConflictDialogType::Conflict_Skeleton); + ConflictDialogType::Conflict_Skeleton, + Brush, + IconTooltip); } } } diff --git a/Engine/Source/Editor/DetailCustomizations/Private/SceneCaptureDetails.cpp b/Engine/Source/Editor/DetailCustomizations/Private/SceneCaptureDetails.cpp index 8243523e23f0..8f86b45d9a4c 100644 --- a/Engine/Source/Editor/DetailCustomizations/Private/SceneCaptureDetails.cpp +++ b/Engine/Source/Editor/DetailCustomizations/Private/SceneCaptureDetails.cpp @@ -96,6 +96,7 @@ void FSceneCaptureDetails::CustomizeDetails( IDetailLayoutBuilder& DetailLayout ShowFlagsToAllowForCaptures.Add(FEngineShowFlags::EShowFlag::SF_MotionBlur); ShowFlagsToAllowForCaptures.Add(FEngineShowFlags::EShowFlag::SF_Bloom); ShowFlagsToAllowForCaptures.Add(FEngineShowFlags::EShowFlag::SF_EyeAdaptation); + ShowFlagsToAllowForCaptures.Add(FEngineShowFlags::EShowFlag::SF_Game); // Create array of flag name strings for each group TArray< TArray > ShowFlagsByGroup; diff --git a/Engine/Source/Editor/DetailCustomizations/Public/Customizations/CurveTableCustomization.h b/Engine/Source/Editor/DetailCustomizations/Public/Customizations/CurveTableCustomization.h index f49cb1b4591f..9b7b17525b9a 100644 --- a/Engine/Source/Editor/DetailCustomizations/Public/Customizations/CurveTableCustomization.h +++ b/Engine/Source/Editor/DetailCustomizations/Public/Customizations/CurveTableCustomization.h @@ -107,7 +107,7 @@ protected: if ( CurveTable != NULL ) { /** Extract all the row names from the RowMap */ - for ( TMap::TConstIterator Iterator(CurveTable->RowMap); Iterator; ++Iterator ) + for ( TMap::TConstIterator Iterator(CurveTable->GetRowMap()); Iterator; ++Iterator ) { /** Create a simple array of the row names */ TSharedRef RowNameItem = MakeShareable(new FString(Iterator.Key().ToString())); @@ -213,7 +213,7 @@ protected: if( CurveTable != NULL ) { /** Extract all the row names from the RowMap */ - for( TMap::TConstIterator Iterator( CurveTable->RowMap ); Iterator; ++Iterator ) + for( TMap::TConstIterator Iterator( CurveTable->GetRowMap() ); Iterator; ++Iterator ) { /** Create a simple array of the row names */ FString RowString = Iterator.Key().ToString(); diff --git a/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.cpp b/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.cpp index 48c7cb935657..1fec6d02a2b9 100644 --- a/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.cpp +++ b/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.cpp @@ -707,8 +707,8 @@ void FCurveEditorViewportClient::MouseMove(FViewport* Viewport, int32 X, int32 Y DistanceDragged += (FMath::Abs(DeltaX) + FMath::Abs(DeltaY)); // Distance mouse just moved in 'curve' units. - float DeltaIn = DeltaX / PixelsPerIn; - float DeltaOut = -DeltaY / PixelsPerOut; + float DeltaIn = (DeltaX / PixelsPerIn)/GetDPIScale(); + float DeltaOut = (-DeltaY / PixelsPerOut)/GetDPIScale(); // If we are panning around, update the Start/End In/Out values for this view. if(bDraggingHandle) @@ -1395,7 +1395,7 @@ FIntPoint FCurveEditorViewportClient::CalcScreenPos(const FVector2D& Val) return Result; } -FVector2D FCurveEditorViewportClient::CalcValuePoint(const FIntPoint& Pos) +FVector2D FCurveEditorViewportClient::CalcValuePoint(const FVector2D& Pos) { FVector2D Result; Result.X = SharedData->StartIn + ((Pos.X - LabelWidth) / PixelsPerIn); @@ -1476,7 +1476,7 @@ int32 FCurveEditorViewportClient::AddNewKeypoint(int32 InCurveIndex, int32 InSub check(InCurveIndex >= 0 && InCurveIndex < SharedData->EdSetup->Tabs[SharedData->EdSetup->ActiveTab].Curves.Num()); FCurveEdEntry& Entry = SharedData->EdSetup->Tabs[SharedData->EdSetup->ActiveTab].Curves[InCurveIndex]; - FVector2D NewKeyVal = CalcValuePoint(ScreenPos); + FVector2D NewKeyVal = CalcValuePoint(FVector2D(ScreenPos) / GetDPIScale()); float NewKeyIn = SnapIn(NewKeyVal.X); FCurveEdInterface* EdInterface = UInterpCurveEdSetup::GetCurveEdInterfacePointer(Entry); diff --git a/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.h b/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.h index a83d555fed10..6205384a67ef 100644 --- a/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.h +++ b/Engine/Source/Editor/DistCurveEditor/Private/CurveEditorViewportClient.h @@ -72,7 +72,7 @@ private: /** Helper methods */ FIntPoint CalcScreenPos(const FVector2D& Val); - FVector2D CalcValuePoint(const FIntPoint& Pos); + FVector2D CalcValuePoint(const FVector2D& Pos); FColor GetLineColor(FCurveEdInterface* EdInterface, float InVal, bool bFloatingPointColor); FVector2D CalcTangentDir(float Tangent); float CalcTangent(const FVector2D& HandleDelta); diff --git a/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.cpp b/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.cpp index 9babfeef9b50..c3106cd28fc0 100644 --- a/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.cpp +++ b/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.cpp @@ -191,6 +191,7 @@ void FSlateEditorStyle::FStyle::Initialize() SetupGraphEditorStyles(); SetupLevelEditorStyle(); SetupPersonaStyle(); + SetupClassThumbnailOverlays(); SetupClassIconsAndThumbnails(); SetupContentBrowserStyle(); SetupLandscapeEditorStyle(); @@ -2894,6 +2895,8 @@ void FSlateEditorStyle::FStyle::SetupGeneralIcons() { Set("Plus", new IMAGE_BRUSH("Icons/PlusSymbol_12x", Icon12x12)); Set("Cross", new IMAGE_BRUSH("Icons/Cross_12x", Icon12x12)); + Set("ArrowUp", new IMAGE_BRUSH("Icons/ArrowUp_12x", Icon12x12)); + Set("ArrowDown", new IMAGE_BRUSH("Icons/ArrowDown_12x", Icon12x12)); } void FSlateEditorStyle::FStyle::SetupWindowStyles() @@ -6013,7 +6016,12 @@ void FSlateEditorStyle::FStyle::SetupPersonaStyle() } #endif // WITH_EDITOR } - + +void FSlateEditorStyle::FStyle::SetupClassThumbnailOverlays() +{ + Set("ClassThumbnailOverlays.SkeletalMesh_NeedSkinning", new IMAGE_BRUSH("Icons/AssetIcons/SkeletalMeshNeedSkinning_16x", Icon16x16)); +} + void FSlateEditorStyle::FStyle::SetupClassIconsAndThumbnails() { #if WITH_EDITOR @@ -6552,6 +6560,7 @@ void FSlateEditorStyle::FStyle::SetupLandscapeEditorStyle() Set("LandscapeEditor.NoiseTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Noise_40x", Icon40x40)); Set("LandscapeEditor.RetopologizeTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Retopologize_40x", Icon40x40)); Set("LandscapeEditor.VisibilityTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Visibility_40x", Icon40x40)); + Set("LandscapeEditor.BPCustomTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Visibility_40x", Icon40x40));// TODO: change icon Set("LandscapeEditor.SculptTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Sculpt_20x", Icon20x20)); Set("LandscapeEditor.PaintTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Paint_20x", Icon20x20)); Set("LandscapeEditor.SmoothTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Smooth_20x", Icon20x20)); @@ -6562,6 +6571,7 @@ void FSlateEditorStyle::FStyle::SetupLandscapeEditorStyle() Set("LandscapeEditor.NoiseTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Noise_20x", Icon20x20)); Set("LandscapeEditor.RetopologizeTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Retopologize_20x", Icon20x20)); Set("LandscapeEditor.VisibilityTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Visibility_20x", Icon20x20)); + Set("LandscapeEditor.BPCustomTool.Small", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Visibility_20x", Icon20x20)); // TODO: change icon Set("LandscapeEditor.SelectComponentTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_Selection_40x", Icon40x40)); Set("LandscapeEditor.AddComponentTool", new IMAGE_BRUSH("Icons/icon_Landscape_Tool_AddComponent_40x", Icon40x40)); @@ -6764,6 +6774,7 @@ void FSlateEditorStyle::FStyle::SetupToolkitStyles() Set( "MaterialEditor.ToggleMaterialStats", new IMAGE_BRUSH( "Icons/icon_MatEd_Stats_40x", Icon40x40 ) ); Set( "MaterialEditor.ToggleMaterialStats.Small", new IMAGE_BRUSH( "Icons/icon_MatEd_Stats_40x", Icon20x20 ) ); + Set("MaterialEditor.ToggleMaterialStats.Tab", new IMAGE_BRUSH("Icons/icon_MatEd_Stats_40x", Icon16x16)); Set( "MaterialEditor.ToggleBuiltinStats", new IMAGE_BRUSH( "Icons/icon_MatEd_BuiltInStats_40x", Icon40x40 ) ); Set( "MaterialEditor.ToggleBuiltinStats.Small", new IMAGE_BRUSH( "Icons/icon_MatEd_BuiltInStats_40x", Icon20x20 ) ); Set( "MaterialEditor.TogglePlatformStats", new IMAGE_BRUSH( "Icons/icon_MobileStats_40x", Icon40x40 ) ); diff --git a/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.h b/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.h index b3c8017d9781..3aa48e057d2a 100644 --- a/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.h +++ b/Engine/Source/Editor/EditorStyle/Private/SlateEditorStyle.h @@ -82,6 +82,7 @@ public: void SetupGraphEditorStyles(); void SetupLevelEditorStyle(); void SetupPersonaStyle(); + void SetupClassThumbnailOverlays(); void SetupClassIconsAndThumbnails(); void SetupContentBrowserStyle(); void SetupLandscapeEditorStyle(); diff --git a/Engine/Source/Editor/GraphEditor/Private/GraphEditorSettings.cpp b/Engine/Source/Editor/GraphEditor/Private/GraphEditorSettings.cpp index d35a02167f2f..07cf3e326c82 100644 --- a/Engine/Source/Editor/GraphEditor/Private/GraphEditorSettings.cpp +++ b/Engine/Source/Editor/GraphEditor/Private/GraphEditorSettings.cpp @@ -36,6 +36,7 @@ UGraphEditorSettings::UGraphEditorSettings( const FObjectInitializer& ObjectInit BytePinTypeColor = FLinearColor(0.0f, 0.160000f, 0.131270f, 1.0f); // dark green ClassPinTypeColor = FLinearColor(0.1f, 0.0f, 0.5f, 1.0f); // deep purple (violet) IntPinTypeColor = FLinearColor(0.013575f, 0.770000f, 0.429609f, 1.0f); // green-blue + Int64PinTypeColor = FLinearColor(0.413575f, 0.770000f, 0.429609f, 1.0f); FloatPinTypeColor = FLinearColor(0.357667f, 1.0f, 0.060000f, 1.0f); // bright green NamePinTypeColor = FLinearColor(0.607717f, 0.224984f, 1.0f, 1.0f); // lilac DelegatePinTypeColor = FLinearColor(1.0f, 0.04f, 0.04f, 1.0f); // bright red diff --git a/Engine/Source/Editor/GraphEditor/Private/NodeFactory.cpp b/Engine/Source/Editor/GraphEditor/Private/NodeFactory.cpp index 1c45f2b0b304..1019b726d910 100644 --- a/Engine/Source/Editor/GraphEditor/Private/NodeFactory.cpp +++ b/Engine/Source/Editor/GraphEditor/Private/NodeFactory.cpp @@ -294,6 +294,10 @@ TSharedPtr FNodeFactory::CreateK2PinWidget(UEdGraphPin* InPin) { return SNew(SGraphPinInteger, InPin); } + else if (InPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int64) + { + return SNew(SGraphPinNum, InPin); + } else if (InPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Float) { return SNew(SGraphPinNum, InPin); diff --git a/Engine/Source/Editor/GraphEditor/Public/GraphEditorSettings.h b/Engine/Source/Editor/GraphEditor/Public/GraphEditorSettings.h index 98986cdb66ce..37e5920435b7 100644 --- a/Engine/Source/Editor/GraphEditor/Public/GraphEditorSettings.h +++ b/Engine/Source/Editor/GraphEditor/Public/GraphEditorSettings.h @@ -148,6 +148,10 @@ public: UPROPERTY(EditAnywhere, config, Category=PinColors) FLinearColor IntPinTypeColor; + /** Integer64 pin type color */ + UPROPERTY(EditAnywhere, config, Category = PinColors) + FLinearColor Int64PinTypeColor; + /** Floating-point pin type color */ UPROPERTY(EditAnywhere, config, Category=PinColors) FLinearColor FloatPinTypeColor; diff --git a/Engine/Source/Editor/Kismet/Private/BlueprintCompilationManager.cpp b/Engine/Source/Editor/Kismet/Private/BlueprintCompilationManager.cpp index b1886120615b..4ba79f13c89a 100644 --- a/Engine/Source/Editor/Kismet/Private/BlueprintCompilationManager.cpp +++ b/Engine/Source/Editor/Kismet/Private/BlueprintCompilationManager.cpp @@ -689,7 +689,11 @@ void FBlueprintCompilationManagerImpl::FlushCompilationQueueImpl(TArrayHasAnyFunctionFlags(EFunctionFlags::FUNC_BlueprintCallable)) { UFunction* NewFunction = BP->SkeletonGeneratedClass->FindFunctionByName((*FuncIt)->GetFName()); - if(NewFunction == nullptr || !NewFunction->IsSignatureCompatibleWith(*FuncIt)) + if( NewFunction == nullptr || + !NewFunction->IsSignatureCompatibleWith(*FuncIt) || + // If a function changes its net flags, callers may now need to do a full EX_FinalFunction/EX_VirtualFunction + // instead of a EX_LocalFinalFunction/EX_LocalVirtualFunction: + NewFunction->HasAnyFunctionFlags(FUNC_NetFuncFlags) != FuncIt->HasAnyFunctionFlags(FUNC_NetFuncFlags)) { BlueprintsWithSignatureChanges.Add(BP); break; diff --git a/Engine/Source/Editor/Kismet/Private/UserDefinedStructureEditor.cpp b/Engine/Source/Editor/Kismet/Private/UserDefinedStructureEditor.cpp index 970baa124111..67ebff37e378 100644 --- a/Engine/Source/Editor/Kismet/Private/UserDefinedStructureEditor.cpp +++ b/Engine/Source/Editor/Kismet/Private/UserDefinedStructureEditor.cpp @@ -152,7 +152,9 @@ public: if (Info != FStructureEditorUtils::DefaultValueChanged) { StructData->Initialize(UserDefinedStruct.Get()); - DetailsView->SetObject(UserDefinedStruct.Get()); + + // Force the set object call because we may be called multiple times in a row if more than one struct was changed at the same time + DetailsView->SetObject(UserDefinedStruct.Get(), true); } UserDefinedStruct.Get()->InitializeDefaultValue(StructData->GetStructMemory()); diff --git a/Engine/Source/Editor/Kismet/Public/BlueprintEditor.h b/Engine/Source/Editor/Kismet/Public/BlueprintEditor.h index b93d8e91ba41..355303408274 100644 --- a/Engine/Source/Editor/Kismet/Public/BlueprintEditor.h +++ b/Engine/Source/Editor/Kismet/Public/BlueprintEditor.h @@ -950,9 +950,6 @@ protected: void OnRenameNode(); bool CanRenameNodes() const; - /* Renames a GraphNode */ - void RenameGraph(class UEdGraphNode* GraphNode, const FString& NewName); - /** Called when a node's title is being committed for a rename so it can be verified */ bool OnNodeVerifyTitleCommit(const FText& NewText, UEdGraphNode* NodeBeingChanged, FText& OutErrorMessage); diff --git a/Engine/Source/Editor/KismetCompiler/Private/AnimBlueprintCompiler.cpp b/Engine/Source/Editor/KismetCompiler/Private/AnimBlueprintCompiler.cpp index 013a3a4b2a01..d893452d09e3 100644 --- a/Engine/Source/Editor/KismetCompiler/Private/AnimBlueprintCompiler.cpp +++ b/Engine/Source/Editor/KismetCompiler/Private/AnimBlueprintCompiler.cpp @@ -2037,7 +2037,10 @@ void FAnimBlueprintCompilerContext::SpawnNewClass(const FString& NewClassName) void FAnimBlueprintCompilerContext::OnPostCDOCompiled() { - FExposedValueHandler::Initialize( NewAnimBlueprintClass->EvaluateGraphExposedInputs, NewAnimBlueprintClass->ClassDefaultObject ); + for (UAnimBlueprintGeneratedClass* ClassWithInputHandlers = NewAnimBlueprintClass; ClassWithInputHandlers != nullptr; ClassWithInputHandlers = Cast(ClassWithInputHandlers->GetSuperClass())) + { + FExposedValueHandler::Initialize(ClassWithInputHandlers->EvaluateGraphExposedInputs, NewAnimBlueprintClass->ClassDefaultObject); + } } void FAnimBlueprintCompilerContext::OnNewClassSet(UBlueprintGeneratedClass* ClassToUse) diff --git a/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerMisc.cpp b/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerMisc.cpp index f7c131898f3e..a323898acab4 100644 --- a/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerMisc.cpp +++ b/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerMisc.cpp @@ -123,6 +123,11 @@ static bool IsTypeCompatibleWithProperty(UEdGraphPin* SourcePin, const FEdGraphP UIntProperty* SpecificProperty = Cast(TestProperty); bTypeMismatch = (SpecificProperty == nullptr); } + else if (PinCategory == UEdGraphSchema_K2::PC_Int64) + { + UInt64Property* SpecificProperty = Cast(TestProperty); + bTypeMismatch = (SpecificProperty == nullptr); + } else if (PinCategory == UEdGraphSchema_K2::PC_Name) { UNameProperty* SpecificProperty = Cast(TestProperty); @@ -1027,6 +1032,11 @@ UProperty* FKismetCompilerUtilities::CreatePrimitiveProperty(UObject* PropertySc NewProperty = NewObject(PropertyScope, ValidatedPropertyName, ObjectFlags); NewProperty->SetPropertyFlags(CPF_HasGetValueTypeHash); } + else if (PinCategory == UEdGraphSchema_K2::PC_Int64) + { + NewProperty = NewObject(PropertyScope, ValidatedPropertyName, ObjectFlags); + NewProperty->SetPropertyFlags(CPF_HasGetValueTypeHash); + } else if (PinCategory == UEdGraphSchema_K2::PC_Float) { NewProperty = NewObject(PropertyScope, ValidatedPropertyName, ObjectFlags); diff --git a/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerVMBackend.cpp b/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerVMBackend.cpp index ef5f66efde15..c2e369a0035e 100644 --- a/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerVMBackend.cpp +++ b/Engine/Source/Editor/KismetCompiler/Private/KismetCompilerVMBackend.cpp @@ -488,7 +488,7 @@ public: { return Property->IsA(); } - return false; + return Type && (Type->PinCategory == UEdGraphSchema_K2::PC_Int64); } static bool IsUInt64(const FEdGraphPinType* Type, const UProperty* Property) @@ -1194,6 +1194,9 @@ public: && !FunctionToCall->GetOuterUClass()->IsChildOf(UInterface::StaticClass()) && FunctionToCall->GetOwnerClass()->GetName() == TEXT("KismetMathLibrary"); + const bool bLocalScriptFunction = + !FunctionToCall->HasAnyFunctionFlags(FUNC_Native|FUNC_NetFuncFlags); + // Handle the function calling context if needed FContextEmitter CallContextWriter(*this); @@ -1217,6 +1220,10 @@ public: { Writer << EX_CallMath; } + else if(bLocalScriptFunction) + { + Writer << EX_LocalFinalFunction; + } else { Writer << EX_FinalFunction; @@ -1227,7 +1234,14 @@ public: else { FName FunctionName(FunctionToCall->GetFName()); - Writer << EX_VirtualFunction; + if(bLocalScriptFunction) + { + Writer << EX_LocalVirtualFunction; + } + else + { + Writer << EX_VirtualFunction; + } Writer << FunctionName; } diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.cpp index 9afa0ddb7550..c7f2177f5698 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.cpp @@ -52,6 +52,9 @@ #include "Framework/Commands/InputBindingManager.h" #include "MouseDeltaTracker.h" #include "Interfaces/IMainFrameModule.h" +#include "LandscapeBPCustomBrush.h" +#include "Engine/TextureRenderTarget2D.h" +#include "Settings/EditorExperimentalSettings.h" #define LOCTEXT_NAMESPACE "Landscape" @@ -97,6 +100,7 @@ IMPLEMENT_HIT_PROXY(HNewLandscapeGrabHandleProxy, HHitProxy) void ALandscape::SplitHeightmap(ULandscapeComponent* Comp, bool bMoveToCurrentLevel /*= false*/) { ULandscapeInfo* Info = Comp->GetLandscapeInfo(); + ALandscape* Landscape = Info->LandscapeActor.Get(); int32 ComponentSizeVerts = Comp->NumSubsections * (Comp->SubsectionSizeQuads + 1); // make sure the heightmap UVs are powers of two. int32 HeightmapSizeU = (1 << FMath::CeilLogTwo(ComponentSizeVerts)); @@ -135,8 +139,114 @@ void ALandscape::SplitHeightmap(ULandscapeComponent* Comp, bool bMoveToCurrentLe } Comp->HeightmapScaleBias = FVector4(1.0f / (float)HeightmapSizeU, 1.0f / (float)HeightmapSizeV, 0.0f, 0.0f); - Comp->HeightmapTexture = HeightmapTexture; +#if WITH_EDITORONLY_DATA + // TODO: There is a bug here with updating the mip regions + /*if (GetMutableDefault()->bProceduralLandscape) + { + // Add component to new heightmap + FRenderDataPerHeightmap* RenderData = Landscape->RenderDataPerHeightmap.Find(HeightmapTexture); + + if (RenderData == nullptr) + { + FRenderDataPerHeightmap NewData; + NewData.Components.Add(Comp); + NewData.TopLeftSectionBase = Comp->GetSectionBase(); + NewData.HeightmapKey = HeightmapTexture; + NewData.HeightmapsCPUReadBack = new FLandscapeProceduralTexture2DCPUReadBackResource(HeightmapTexture->Source.GetSizeX(), HeightmapTexture->Source.GetSizeY(), Comp->GetHeightmap(false)->GetPixelFormat(), HeightmapTexture->Source.GetNumMips()); + BeginInitResource(NewData.HeightmapsCPUReadBack); + + Landscape->RenderDataPerHeightmap.Add(HeightmapTexture, NewData); + } + else + { + for (ULandscapeComponent* Component : RenderData->Components) + { + RenderData->TopLeftSectionBase.X = FMath::Min(RenderData->TopLeftSectionBase.X, Component->GetSectionBase().X); + RenderData->TopLeftSectionBase.Y = FMath::Min(RenderData->TopLeftSectionBase.Y, Component->GetSectionBase().Y); + } + + RenderData->Components.Add(Comp); + } + + // Remove component from old heightmap + FRenderDataPerHeightmap* OldRenderData = Landscape->RenderDataPerHeightmap.Find(Comp->GetHeightmap(false)); + OldRenderData->Components.Remove(Comp); + + bool RemoveLayerHeightmap = false; + + // No component use this heightmap anymore, so clean up + if (OldRenderData->Components.Num() == 0) + { + ReleaseResourceAndFlush(OldRenderData->HeightmapsCPUReadBack); + delete OldRenderData->HeightmapsCPUReadBack; + OldRenderData->HeightmapsCPUReadBack = nullptr; + Landscape->RenderDataPerHeightmap.Remove(Comp->GetHeightmap(false)); + RemoveLayerHeightmap = true; + } + + // Move layer content to new layer heightmap + for (FLandscapeLayer& Layer : Landscape->LandscapeLayers) + { + UTexture2D** OldLayerHeightmap = Layer.HeightmapTarget.Heightmaps.Find(Comp->GetHeightmap(false)); + + if (OldLayerHeightmap != nullptr) + { + // If we remove the main heightmap, remove the layers one, as they exist on a 1 to 1 + if (RemoveLayerHeightmap) + { + Layer.HeightmapTarget.Heightmaps.Remove(*OldLayerHeightmap); + } + + // Read old data and split + TArray LayerHeightData; + LayerHeightData.AddZeroed((1 + Comp->ComponentSizeQuads)*(1 + Comp->ComponentSizeQuads) * sizeof(uint16)); + // Because of edge problem, normal would be just copy from old component data + TArray LayerNormalData; + LayerNormalData.AddZeroed((1 + Comp->ComponentSizeQuads)*(1 + Comp->ComponentSizeQuads) * sizeof(uint16)); + LandscapeEdit.GetHeightDataFast(Comp->GetSectionBase().X, Comp->GetSectionBase().Y, Comp->GetSectionBase().X + Comp->ComponentSizeQuads, Comp->GetSectionBase().Y + Comp->ComponentSizeQuads, (uint16*)LayerHeightData.GetData(), 0, (uint16*)LayerNormalData.GetData(), *OldLayerHeightmap); + + UTexture2D** NewLayerHeightmap = Layer.HeightmapTarget.Heightmaps.Find(HeightmapTexture); + + if (NewLayerHeightmap == nullptr) + { + UTexture2D* LayerHeightmapTexture = Comp->GetLandscapeProxy()->CreateLandscapeTexture(HeightmapSizeU, HeightmapSizeV, TEXTUREGROUP_Terrain_Heightmap, TSF_BGRA8, bMoveToCurrentLevel ? Comp->GetWorld()->GetCurrentLevel()->GetOutermost() : nullptr); + + MipSubsectionSizeQuads = Comp->SubsectionSizeQuads; + MipSizeU = HeightmapSizeU; + MipSizeV = HeightmapSizeV; + int32 MipIndex = 0; + + while (MipSizeU > 1 && MipSizeV > 1 && MipSubsectionSizeQuads >= 1) + { + FColor* HeightmapTextureData = (FColor*)LayerHeightmapTexture->Source.LockMip(MipIndex); + FMemory::Memzero(HeightmapTextureData, MipSizeU*MipSizeV * sizeof(FColor)); + LayerHeightmapTexture->Source.UnlockMip(MipIndex); + + MipSizeU >>= 1; + MipSizeV >>= 1; + + MipSubsectionSizeQuads = ((MipSubsectionSizeQuads + 1) >> 1) - 1; + ++MipIndex; + } + + Layer.HeightmapTarget.Heightmaps.Add(HeightmapTexture, LayerHeightmapTexture); + + LandscapeEdit.SetHeightData(Comp->GetSectionBase().X, Comp->GetSectionBase().Y, Comp->GetSectionBase().X + Comp->ComponentSizeQuads, Comp->GetSectionBase().Y + Comp->ComponentSizeQuads, (uint16*)LayerHeightData.GetData(), 0, false, (uint16*)LayerNormalData.GetData(), + false, LayerHeightmapTexture, nullptr, true, true, true); + } + else + { + LandscapeEdit.SetHeightData(Comp->GetSectionBase().X, Comp->GetSectionBase().Y, Comp->GetSectionBase().X + Comp->ComponentSizeQuads, Comp->GetSectionBase().Y + Comp->ComponentSizeQuads, (uint16*)LayerHeightData.GetData(), 0, false, (uint16*)LayerNormalData.GetData(), + false, *NewLayerHeightmap, nullptr, true, true, true); + } + } + } + } + */ +#endif + + Comp->SetHeightmap(HeightmapTexture); Comp->UpdateMaterialInstances(); for (int32 i = 0; i < HeightmapTextureMipData.Num(); i++) @@ -222,6 +332,7 @@ FEdModeLandscape::FEdModeLandscape() InitializeTool_Splines(); InitializeTool_Ramp(); InitializeTool_Mirror(); + InitializeTool_BPCustom(); CurrentTool = nullptr; CurrentToolIndex = INDEX_NONE; @@ -310,6 +421,12 @@ void FEdModeLandscape::InitializeToolModes() ToolMode_Sculpt->ValidTools.Add(TEXT("HydraErosion")); ToolMode_Sculpt->ValidTools.Add(TEXT("Retopologize")); ToolMode_Sculpt->ValidTools.Add(TEXT("Visibility")); + + if (GetMutableDefault()->bProceduralLandscape) + { + ToolMode_Sculpt->ValidTools.Add(TEXT("BPCustom")); + } + ToolMode_Sculpt->ValidTools.Add(TEXT("Mask")); ToolMode_Sculpt->ValidTools.Add(TEXT("CopyPaste")); ToolMode_Sculpt->ValidTools.Add(TEXT("Mirror")); @@ -320,6 +437,11 @@ void FEdModeLandscape::InitializeToolModes() ToolMode_Paint->ValidTools.Add(TEXT("Flatten")); ToolMode_Paint->ValidTools.Add(TEXT("Noise")); ToolMode_Paint->ValidTools.Add(TEXT("Visibility")); + + if (GetMutableDefault()->bProceduralLandscape) + { + ToolMode_Paint->ValidTools.Add(TEXT("BPCustom")); + } } bool FEdModeLandscape::UsesToolkits() const @@ -339,6 +461,9 @@ void FEdModeLandscape::Enter() // Call parent implementation FEdMode::Enter(); + OnLevelActorDeletedDelegateHandle = GEngine->OnLevelActorDeleted().AddSP(this, &FEdModeLandscape::OnLevelActorRemoved); + OnLevelActorAddedDelegateHandle = GEngine->OnLevelActorAdded().AddSP(this, &FEdModeLandscape::OnLevelActorAdded); + ALandscapeProxy* SelectedLandscape = GEditor->GetSelectedActors()->GetTop(); if (SelectedLandscape) { @@ -376,6 +501,16 @@ void FEdModeLandscape::Enter() { ALandscapeProxy* LandscapeProxy = CurrentToolTarget.LandscapeInfo->GetLandscapeProxy(); LandscapeProxy->OnMaterialChangedDelegate().AddRaw(this, &FEdModeLandscape::OnLandscapeMaterialChangedDelegate); + + if (GetMutableDefault()->bProceduralLandscape) + { + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape != nullptr) + { + Landscape->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::All_Render); + } + } } if (CurrentGizmoActor.IsValid()) @@ -526,6 +661,9 @@ void FEdModeLandscape::Exit() ViewportWorldInteraction->OnViewportInteractionInputAction().RemoveAll(this); ViewportWorldInteraction->OnViewportInteractionHoverUpdate().RemoveAll(this); } + + GEngine->OnLevelActorDeleted().Remove(OnLevelActorDeletedDelegateHandle); + GEngine->OnLevelActorAdded().Remove(OnLevelActorAddedDelegateHandle); FEditorSupportDelegates::WorldChange.Remove(OnWorldChangeDelegateHandle); GetWorld()->OnLevelsChanged().Remove(OnLevelsChangedDelegateHandle); @@ -1299,24 +1437,38 @@ bool FEdModeLandscape::ProcessEditCopy() if (!Result) { - bool IsSlowTask = IsSlowSelect(CurrentGizmoActor->TargetLandscapeInfo); - if (IsSlowTask) + TArray CurrentlySelectedBPBrush; + for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { - GWarn->BeginSlowTask(LOCTEXT("BeginFitGizmoAndCopy", "Fit Gizmo to Selected Region and Copy Data..."), true); + ALandscapeBlueprintCustomBrush* Actor = StaticCast(*It); + + if (Actor != nullptr) + { + CurrentlySelectedBPBrush.Add(Actor); + } } - FScopedTransaction Transaction(LOCTEXT("LandscapeGizmo_Copy", "Copy landscape data to Gizmo")); - CurrentGizmoActor->Modify(); - CurrentGizmoActor->FitToSelection(); - CopyDataToGizmo(); - SetCurrentTool(FName("CopyPaste")); - - if (IsSlowTask) + if (CurrentlySelectedBPBrush.Num() == 0) { - GWarn->EndSlowTask(); - } + bool IsSlowTask = IsSlowSelect(CurrentGizmoActor->TargetLandscapeInfo); + if (IsSlowTask) + { + GWarn->BeginSlowTask(LOCTEXT("BeginFitGizmoAndCopy", "Fit Gizmo to Selected Region and Copy Data..."), true); + } - Result = true; + FScopedTransaction Transaction(LOCTEXT("LandscapeGizmo_Copy", "Copy landscape data to Gizmo")); + CurrentGizmoActor->Modify(); + CurrentGizmoActor->FitToSelection(); + CopyDataToGizmo(); + SetCurrentTool(FName("CopyPaste")); + + if (IsSlowTask) + { + GWarn->EndSlowTask(); + } + + Result = true; + } } } @@ -1341,19 +1493,33 @@ bool FEdModeLandscape::ProcessEditPaste() if (!Result) { - bool IsSlowTask = IsSlowSelect(CurrentGizmoActor->TargetLandscapeInfo); - if (IsSlowTask) + TArray CurrentlySelectedBPBrush; + for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { - GWarn->BeginSlowTask(LOCTEXT("BeginPasteGizmoDataTask", "Paste Gizmo Data..."), true); - } - PasteDataFromGizmo(); - SetCurrentTool(FName("CopyPaste")); - if (IsSlowTask) - { - GWarn->EndSlowTask(); + ALandscapeBlueprintCustomBrush* Actor = StaticCast(*It); + + if (Actor != nullptr) + { + CurrentlySelectedBPBrush.Add(Actor); + } } - Result = true; + if (CurrentlySelectedBPBrush.Num() == 0) + { + bool IsSlowTask = IsSlowSelect(CurrentGizmoActor->TargetLandscapeInfo); + if (IsSlowTask) + { + GWarn->BeginSlowTask(LOCTEXT("BeginPasteGizmoDataTask", "Paste Gizmo Data..."), true); + } + PasteDataFromGizmo(); + SetCurrentTool(FName("CopyPaste")); + if (IsSlowTask) + { + GWarn->EndSlowTask(); + } + + Result = true; + } } } @@ -1958,6 +2124,14 @@ void FEdModeLandscape::SetCurrentTool(int32 ToolIndex) GEditor->RedrawLevelEditingViewports(); } +void FEdModeLandscape::RefreshDetailPanel() +{ + if (Toolkit.IsValid()) + { + StaticCastSharedPtr(Toolkit)->RefreshDetailPanel(); + } +} + void FEdModeLandscape::SetCurrentBrushSet(FName BrushSetName) { for (int32 BrushIndex = 0; BrushIndex < LandscapeBrushSets.Num(); BrushIndex++) @@ -2102,6 +2276,8 @@ int32 FEdModeLandscape::UpdateLandscapeList() CurrentToolTarget.LandscapeInfo = LandscapeList[0].Info; CurrentIndex = 0; + SetCurrentProceduralLayer(0); + // Init UI to saved value ALandscapeProxy* LandscapeProxy = CurrentToolTarget.LandscapeInfo->GetLandscapeProxy(); @@ -2147,7 +2323,7 @@ void FEdModeLandscape::UpdateTargetList() bool bFoundSelected = false; // Add heightmap - LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(LOCTEXT("Heightmap", "Heightmap"), ELandscapeToolTargetType::Heightmap, CurrentToolTarget.LandscapeInfo.Get()))); + LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(LOCTEXT("Heightmap", "Heightmap"), ELandscapeToolTargetType::Heightmap, CurrentToolTarget.LandscapeInfo.Get(), CurrentToolTarget.CurrentProceduralLayerIndex))); if (CurrentToolTarget.TargetType == ELandscapeToolTargetType::Heightmap) { @@ -2156,7 +2332,7 @@ void FEdModeLandscape::UpdateTargetList() // Add visibility FLandscapeInfoLayerSettings VisibilitySettings(ALandscapeProxy::VisibilityLayer, LandscapeProxy); - LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(LOCTEXT("Visibility", "Visibility"), ELandscapeToolTargetType::Visibility, VisibilitySettings))); + LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(LOCTEXT("Visibility", "Visibility"), ELandscapeToolTargetType::Visibility, VisibilitySettings, CurrentToolTarget.CurrentProceduralLayerIndex))); if (CurrentToolTarget.TargetType == ELandscapeToolTargetType::Visibility) { @@ -2207,7 +2383,7 @@ void FEdModeLandscape::UpdateTargetList() } // Add the layer - LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(FText::FromName(LayerName), ELandscapeToolTargetType::Weightmap, LayerSettings))); + LandscapeTargetList.Add(MakeShareable(new FLandscapeTargetListInfo(FText::FromName(LayerName), ELandscapeToolTargetType::Weightmap, LayerSettings, CurrentToolTarget.CurrentProceduralLayerIndex))); } if (!bFoundSelected) @@ -2868,7 +3044,7 @@ FVector FEdModeLandscape::GetWidgetLocation() const } // Override Widget for Splines Tool - if (CurrentTool) + if (CurrentTool && CurrentTool->OverrideWidgetLocation()) { return CurrentTool->GetWidgetLocation(); } @@ -2885,7 +3061,7 @@ bool FEdModeLandscape::GetCustomDrawingCoordinateSystem(FMatrix& InMatrix, void* } // Override Widget for Splines Tool - if (CurrentTool) + if (CurrentTool && CurrentTool->OverrideWidgetRotation()) { InMatrix = CurrentTool->GetWidgetRotation(); return true; @@ -2968,6 +3144,10 @@ bool FEdModeLandscape::IsSelectionAllowed(AActor* InActor, bool bInSelection) co { return true; } + else if (InActor->IsA(ALandscapeBlueprintCustomBrush::StaticClass())) + { + return true; + } return true; } @@ -3151,6 +3331,11 @@ void FEdModeLandscape::ImportData(const FLandscapeTargetListInfo& TargetInfo, co return; } + if (GetMutableDefault()->bProceduralLandscape) + { + ChangeHeightmapsToCurrentProceduralLayerHeightmaps(); + } + TArray Data; if (ImportResolution != LandscapeResolution) { @@ -3175,6 +3360,14 @@ void FEdModeLandscape::ImportData(const FLandscapeTargetListInfo& TargetInfo, co FHeightmapAccessor HeightmapAccessor(LandscapeInfo); HeightmapAccessor.SetData(MinX, MinY, MaxX, MaxY, Data.GetData()); + + if (GetMutableDefault()->bProceduralLandscape) + { + ChangeHeightmapsToCurrentProceduralLayerHeightmaps(true); + + check(CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()); + CurrentToolTarget.LandscapeInfo->LandscapeActor.Get()->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::Heightmap_All); + } } else { @@ -3314,8 +3507,8 @@ void FEdModeLandscape::DeleteLandscapeComponents(ULandscapeInfo* LandscapeInfo, // Search neighbor only for (ULandscapeComponent* Component : ComponentsToDelete) { - int32 SearchX = Component->HeightmapTexture->Source.GetSizeX() / NeedHeightmapSize; - int32 SearchY = Component->HeightmapTexture->Source.GetSizeY() / NeedHeightmapSize; + int32 SearchX = Component->GetHeightmap()->Source.GetSizeX() / NeedHeightmapSize; + int32 SearchY = Component->GetHeightmap()->Source.GetSizeY() / NeedHeightmapSize; FIntPoint ComponentBase = Component->GetSectionBase() / Component->ComponentSizeQuads; for (int32 Y = 0; Y < SearchY; ++Y) @@ -3328,7 +3521,7 @@ void FEdModeLandscape::DeleteLandscapeComponents(ULandscapeInfo* LandscapeInfo, int32 XDir = (Dir >> 1) ? 1 : -1; int32 YDir = (Dir % 2) ? 1 : -1; ULandscapeComponent* Neighbor = LandscapeInfo->XYtoComponentMap.FindRef(ComponentBase + FIntPoint(XDir*X, YDir*Y)); - if (Neighbor && Neighbor->HeightmapTexture == Component->HeightmapTexture && !HeightmapUpdateComponents.Contains(Neighbor)) + if (Neighbor && Neighbor->GetHeightmap() == Component->GetHeightmap() && !HeightmapUpdateComponents.Contains(Neighbor)) { Neighbor->Modify(); HeightmapUpdateComponents.Add(Neighbor); @@ -3393,12 +3586,14 @@ void FEdModeLandscape::DeleteLandscapeComponents(ULandscapeInfo* LandscapeInfo, } } - if (Component->HeightmapTexture) + UTexture2D* HeightmapTexture = Component->GetHeightmap(); + + if (HeightmapTexture) { - Component->HeightmapTexture->SetFlags(RF_Transactional); - Component->HeightmapTexture->Modify(); - Component->HeightmapTexture->MarkPackageDirty(); - Component->HeightmapTexture->ClearFlags(RF_Standalone); // Remove when there is no reference for this Heightmap... + HeightmapTexture->SetFlags(RF_Transactional); + HeightmapTexture->Modify(); + HeightmapTexture->MarkPackageDirty(); + HeightmapTexture->ClearFlags(RF_Standalone); // Remove when there is no reference for this Heightmap... } for (int32 i = 0; i < Component->WeightmapTextures.Num(); ++i) @@ -3459,7 +3654,6 @@ ALandscape* FEdModeLandscape::ChangeComponentSetting(int32 NumComponentsX, int32 const int32 NewVertsX = NumComponentsX * NewComponentSizeQuads + 1; const int32 NewVertsY = NumComponentsY * NewComponentSizeQuads + 1; - FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); TArray HeightData; TArray ImportLayerInfos; FVector LandscapeOffset = FVector::ZeroVector; @@ -3467,87 +3661,92 @@ ALandscape* FEdModeLandscape::ChangeComponentSetting(int32 NumComponentsX, int32 float LandscapeScaleFactor = 1.0f; int32 NewMinX, NewMinY, NewMaxX, NewMaxY; - if (bResample) - { - NewMinX = OldMinX / LandscapeInfo->ComponentSizeQuads * NewComponentSizeQuads; - NewMinY = OldMinY / LandscapeInfo->ComponentSizeQuads * NewComponentSizeQuads; - NewMaxX = NewMinX + NewVertsX - 1; - NewMaxY = NewMinY + NewVertsY - 1; - HeightData.AddZeroed(OldVertsX * OldVertsY * sizeof(uint16)); + { // Scope to flush the texture update before doing the import + FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo); - // GetHeightData alters its args, so make temp copies to avoid screwing things up - int32 TMinX = OldMinX, TMinY = OldMinY, TMaxX = OldMaxX, TMaxY = OldMaxY; - LandscapeEdit.GetHeightData(TMinX, TMinY, TMaxX, TMaxY, HeightData.GetData(), 0); - - HeightData = LandscapeEditorUtils::ResampleData(HeightData, - OldVertsX, OldVertsY, NewVertsX, NewVertsY); - - for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers) + if (bResample) { - if (LayerSettings.LayerInfoObj != NULL) + NewMinX = OldMinX / LandscapeInfo->ComponentSizeQuads * NewComponentSizeQuads; + NewMinY = OldMinY / LandscapeInfo->ComponentSizeQuads * NewComponentSizeQuads; + NewMaxX = NewMinX + NewVertsX - 1; + NewMaxY = NewMinY + NewVertsY - 1; + + HeightData.AddZeroed(OldVertsX * OldVertsY * sizeof(uint16)); + + // GetHeightData alters its args, so make temp copies to avoid screwing things up + int32 TMinX = OldMinX, TMinY = OldMinY, TMaxX = OldMaxX, TMaxY = OldMaxY; + LandscapeEdit.GetHeightData(TMinX, TMinY, TMaxX, TMaxY, HeightData.GetData(), 0); + + HeightData = LandscapeEditorUtils::ResampleData(HeightData, + OldVertsX, OldVertsY, NewVertsX, NewVertsY); + + for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers) { - auto ImportLayerInfo = new(ImportLayerInfos)FLandscapeImportLayerInfo(LayerSettings); - ImportLayerInfo->LayerData.AddZeroed(OldVertsX * OldVertsY * sizeof(uint8)); + if (LayerSettings.LayerInfoObj != NULL) + { + auto ImportLayerInfo = new(ImportLayerInfos)FLandscapeImportLayerInfo(LayerSettings); + ImportLayerInfo->LayerData.AddZeroed(OldVertsX * OldVertsY * sizeof(uint8)); - TMinX = OldMinX; TMinY = OldMinY; TMaxX = OldMaxX; TMaxY = OldMaxY; - LandscapeEdit.GetWeightData(LayerSettings.LayerInfoObj, TMinX, TMinY, TMaxX, TMaxY, ImportLayerInfo->LayerData.GetData(), 0); + TMinX = OldMinX; TMinY = OldMinY; TMaxX = OldMaxX; TMaxY = OldMaxY; + LandscapeEdit.GetWeightData(LayerSettings.LayerInfoObj, TMinX, TMinY, TMaxX, TMaxY, ImportLayerInfo->LayerData.GetData(), 0); - ImportLayerInfo->LayerData = LandscapeEditorUtils::ResampleData(ImportLayerInfo->LayerData, - OldVertsX, OldVertsY, - NewVertsX, NewVertsY); + ImportLayerInfo->LayerData = LandscapeEditorUtils::ResampleData(ImportLayerInfo->LayerData, + OldVertsX, OldVertsY, + NewVertsX, NewVertsY); + } } + + LandscapeScaleFactor = (float)OldLandscapeProxy->ComponentSizeQuads / NewComponentSizeQuads; } - - LandscapeScaleFactor = (float)OldLandscapeProxy->ComponentSizeQuads / NewComponentSizeQuads; - } - else - { - NewMinX = OldMinX + (OldVertsX - NewVertsX) / 2; - NewMinY = OldMinY + (OldVertsY - NewVertsY) / 2; - NewMaxX = NewMinX + NewVertsX - 1; - NewMaxY = NewMinY + NewVertsY - 1; - const int32 RequestedMinX = FMath::Max(OldMinX, NewMinX); - const int32 RequestedMinY = FMath::Max(OldMinY, NewMinY); - const int32 RequestedMaxX = FMath::Min(OldMaxX, NewMaxX); - const int32 RequestedMaxY = FMath::Min(OldMaxY, NewMaxY); - - const int32 RequestedVertsX = RequestedMaxX - RequestedMinX + 1; - const int32 RequestedVertsY = RequestedMaxY - RequestedMinY + 1; - - HeightData.AddZeroed(RequestedVertsX * RequestedVertsY * sizeof(uint16)); - - // GetHeightData alters its args, so make temp copies to avoid screwing things up - int32 TMinX = RequestedMinX, TMinY = RequestedMinY, TMaxX = RequestedMaxX, TMaxY = RequestedMaxY; - LandscapeEdit.GetHeightData(TMinX, TMinY, TMaxX, OldMaxY, HeightData.GetData(), 0); - - HeightData = LandscapeEditorUtils::ExpandData(HeightData, - RequestedMinX, RequestedMinY, RequestedMaxX, RequestedMaxY, - NewMinX, NewMinY, NewMaxX, NewMaxY); - - for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers) + else { - if (LayerSettings.LayerInfoObj != NULL) + NewMinX = OldMinX + (OldVertsX - NewVertsX) / 2; + NewMinY = OldMinY + (OldVertsY - NewVertsY) / 2; + NewMaxX = NewMinX + NewVertsX - 1; + NewMaxY = NewMinY + NewVertsY - 1; + const int32 RequestedMinX = FMath::Max(OldMinX, NewMinX); + const int32 RequestedMinY = FMath::Max(OldMinY, NewMinY); + const int32 RequestedMaxX = FMath::Min(OldMaxX, NewMaxX); + const int32 RequestedMaxY = FMath::Min(OldMaxY, NewMaxY); + + const int32 RequestedVertsX = RequestedMaxX - RequestedMinX + 1; + const int32 RequestedVertsY = RequestedMaxY - RequestedMinY + 1; + + HeightData.AddZeroed(RequestedVertsX * RequestedVertsY * sizeof(uint16)); + + // GetHeightData alters its args, so make temp copies to avoid screwing things up + int32 TMinX = RequestedMinX, TMinY = RequestedMinY, TMaxX = RequestedMaxX, TMaxY = RequestedMaxY; + LandscapeEdit.GetHeightData(TMinX, TMinY, TMaxX, OldMaxY, HeightData.GetData(), 0); + + HeightData = LandscapeEditorUtils::ExpandData(HeightData, + RequestedMinX, RequestedMinY, RequestedMaxX, RequestedMaxY, + NewMinX, NewMinY, NewMaxX, NewMaxY); + + for (const FLandscapeInfoLayerSettings& LayerSettings : LandscapeInfo->Layers) { - auto ImportLayerInfo = new(ImportLayerInfos)FLandscapeImportLayerInfo(LayerSettings); - ImportLayerInfo->LayerData.AddZeroed(NewVertsX * NewVertsY * sizeof(uint8)); + if (LayerSettings.LayerInfoObj != NULL) + { + auto ImportLayerInfo = new(ImportLayerInfos)FLandscapeImportLayerInfo(LayerSettings); + ImportLayerInfo->LayerData.AddZeroed(NewVertsX * NewVertsY * sizeof(uint8)); - TMinX = RequestedMinX; TMinY = RequestedMinY; TMaxX = RequestedMaxX; TMaxY = RequestedMaxY; - LandscapeEdit.GetWeightData(LayerSettings.LayerInfoObj, TMinX, TMinY, TMaxX, TMaxY, ImportLayerInfo->LayerData.GetData(), 0); + TMinX = RequestedMinX; TMinY = RequestedMinY; TMaxX = RequestedMaxX; TMaxY = RequestedMaxY; + LandscapeEdit.GetWeightData(LayerSettings.LayerInfoObj, TMinX, TMinY, TMaxX, TMaxY, ImportLayerInfo->LayerData.GetData(), 0); - ImportLayerInfo->LayerData = LandscapeEditorUtils::ExpandData(ImportLayerInfo->LayerData, - RequestedMinX, RequestedMinY, RequestedMaxX, RequestedMaxY, - NewMinX, NewMinY, NewMaxX, NewMaxY); + ImportLayerInfo->LayerData = LandscapeEditorUtils::ExpandData(ImportLayerInfo->LayerData, + RequestedMinX, RequestedMinY, RequestedMaxX, RequestedMaxY, + NewMinX, NewMinY, NewMaxX, NewMaxY); + } } - } - // offset landscape to component boundary - LandscapeOffset = FVector(NewMinX, NewMinY, 0) * OldLandscapeProxy->GetActorScale(); - LandscapeOffsetQuads = FIntPoint(NewMinX, NewMinY); - NewMinX = 0; - NewMinY = 0; - NewMaxX = NewVertsX - 1; - NewMaxY = NewVertsY - 1; + // offset landscape to component boundary + LandscapeOffset = FVector(NewMinX, NewMinY, 0) * OldLandscapeProxy->GetActorScale(); + LandscapeOffsetQuads = FIntPoint(NewMinX, NewMinY); + NewMinX = 0; + NewMinY = 0; + NewMaxX = NewVertsX - 1; + NewMaxY = NewVertsY - 1; + } } Progress.EnterProgressFrame(CurrentTaskProgress++); @@ -3768,6 +3967,486 @@ ELandscapeEditingState FEdModeLandscape::GetEditingState() const return ELandscapeEditingState::Enabled; } +int32 FEdModeLandscape::GetProceduralLayerCount() const +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return 0; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr) + { + return 0; + } + + return Landscape->ProceduralLayers.Num(); +} + +void FEdModeLandscape::SetCurrentProceduralLayer(int32 InLayerIndex) +{ + CurrentToolTarget.CurrentProceduralLayerIndex = InLayerIndex; + + RefreshDetailPanel(); +} + +int32 FEdModeLandscape::GetCurrentProceduralLayerIndex() const +{ + return CurrentToolTarget.CurrentProceduralLayerIndex; +} + +FName FEdModeLandscape::GetProceduralLayerName(int32 InLayerIndex) const +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return NAME_None; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return NAME_None; + } + + return Landscape->ProceduralLayers[InLayerIndex].Name; +} + +FName FEdModeLandscape::GetCurrentProceduralLayerName() const +{ + return GetProceduralLayerName(CurrentToolTarget.CurrentProceduralLayerIndex); +} + +void FEdModeLandscape::SetProceduralLayerName(int32 InLayerIndex, const FName& InName) +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return; + } + + Landscape->ProceduralLayers[InLayerIndex].Name = InName; +} + +float FEdModeLandscape::GetProceduralLayerWeight(int32 InLayerIndex) const +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return 1.0f; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return 1.0f; + } + + return Landscape->ProceduralLayers[InLayerIndex].Weight; +} + +void FEdModeLandscape::SetProceduralLayerWeight(float InWeight, int32 InLayerIndex) +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return; + } + + Landscape->ProceduralLayers[InLayerIndex].Weight = InWeight; + + Landscape->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::Heightmap_All); +} + +void FEdModeLandscape::SetProceduralLayerVisibility(bool InVisible, int32 InLayerIndex) +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return; + } + + Landscape->ProceduralLayers[InLayerIndex].Visible = InVisible; + + Landscape->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::Heightmap_All); +} + +bool FEdModeLandscape::IsProceduralLayerVisible(int32 InLayerIndex) const +{ + if (CurrentToolTarget.LandscapeInfo == nullptr) + { + return true; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr || !Landscape->ProceduralLayers.IsValidIndex(InLayerIndex)) + { + return true; + } + + return Landscape->ProceduralLayers[InLayerIndex].Visible; +} + +void FEdModeLandscape::AddBrushToCurrentProceduralLayer(int32 InTargetType, ALandscapeBlueprintCustomBrush* InBrush) +{ + if (CurrentToolTarget.LandscapeInfo == nullptr || !CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return; + } + + int32 AddedIndex = Layer->Brushes.Add(FLandscapeProceduralLayerBrush(InBrush)); + + if (InTargetType == ELandscapeToolTargetType::Type::Heightmap) + { + Layer->HeightmapBrushOrderIndices.Add(AddedIndex); + } + else + { + Layer->WeightmapBrushOrderIndices.Add(AddedIndex); + } + + InBrush->SetOwningLandscape(Landscape); + + Landscape->RequestProceduralContentUpdate(InTargetType == ELandscapeToolTargetType::Type::Heightmap ? EProceduralContentUpdateFlag::Heightmap_All : EProceduralContentUpdateFlag::Weightmap_All); +} + +void FEdModeLandscape::RequestProceduralContentUpdate() +{ + if (CurrentToolTarget.LandscapeInfo == nullptr || !CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + Landscape->RequestProceduralContentUpdate(CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap ? EProceduralContentUpdateFlag::Heightmap_All : EProceduralContentUpdateFlag::Weightmap_All); +} + +void FEdModeLandscape::RemoveBrushFromCurrentProceduralLayer(int32 InTargetType, ALandscapeBlueprintCustomBrush* InBrush) +{ + if (CurrentToolTarget.LandscapeInfo == nullptr || !CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + return; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return; + } + + int32 IndexToRemove = INDEX_NONE; + for (int32 i = 0; i < Layer->Brushes.Num(); ++i) + { + if (Layer->Brushes[i].BPCustomBrush == InBrush) + { + IndexToRemove = i; + break; + } + } + + if (IndexToRemove != INDEX_NONE) + { + Layer->Brushes.RemoveAt(IndexToRemove); + + if (InTargetType == ELandscapeToolTargetType::Type::Heightmap) + { + for (int32 i = 0; i < Layer->HeightmapBrushOrderIndices.Num(); ++i) + { + if (Layer->HeightmapBrushOrderIndices[i] == IndexToRemove) + { + // Update the value of the index of all the one after the one we removed, so index still correctly match actual brushes list + for (int32 j = 0; j < Layer->HeightmapBrushOrderIndices.Num(); ++j) + { + if (Layer->HeightmapBrushOrderIndices[j] > IndexToRemove) + { + --Layer->HeightmapBrushOrderIndices[j]; + } + } + + Layer->HeightmapBrushOrderIndices.RemoveAt(i); + break; + } + } + } + else + { + for (int32 i = 0; i < Layer->WeightmapBrushOrderIndices.Num(); ++i) + { + if (Layer->WeightmapBrushOrderIndices[i] == IndexToRemove) + { + // Update the value of the index of all the one after the one we removed, so index still correctly match actual brushes list + for (int32 j = 0; j < Layer->WeightmapBrushOrderIndices.Num(); ++j) + { + if (Layer->WeightmapBrushOrderIndices[j] > IndexToRemove) + { + --Layer->HeightmapBrushOrderIndices[j]; + } + } + + Layer->WeightmapBrushOrderIndices.RemoveAt(i); + break; + } + } + } + + InBrush->SetOwningLandscape(nullptr); + } + + Landscape->RequestProceduralContentUpdate(InTargetType == ELandscapeToolTargetType::Type::Heightmap ? EProceduralContentUpdateFlag::Heightmap_All : EProceduralContentUpdateFlag::Weightmap_All); +} + +bool FEdModeLandscape::AreAllBrushesCommitedToCurrentProceduralLayer(int32 InTargetType) +{ + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return false; + } + + for (FLandscapeProceduralLayerBrush& Brush : Layer->Brushes) + { + if (!Brush.BPCustomBrush->IsCommited() + && ((InTargetType == ELandscapeToolTargetType::Type::Heightmap && Brush.BPCustomBrush->IsAffectingHeightmap()) || (InTargetType == ELandscapeToolTargetType::Type::Weightmap && Brush.BPCustomBrush->IsAffectingWeightmap()))) + { + return false; + } + } + + return true; +} + +void FEdModeLandscape::SetCurrentProceduralLayerBrushesCommitState(int32 InTargetType, bool InCommited) +{ + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return; + } + + for (FLandscapeProceduralLayerBrush& Brush : Layer->Brushes) + { + Brush.BPCustomBrush->SetCommitState(InCommited); + } + + GEngine->BroadcastLevelActorListChanged(); +} + +TArray& FEdModeLandscape::GetBrushesOrderForCurrentProceduralLayer(int32 InTargetType) const +{ + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + check(Layer); + + if (InTargetType == ELandscapeToolTargetType::Type::Heightmap) + { + return Layer->HeightmapBrushOrderIndices; + } + else + { + return Layer->WeightmapBrushOrderIndices; + } +} + +ALandscapeBlueprintCustomBrush* FEdModeLandscape::GetBrushForCurrentProceduralLayer(int32 InTargetType, int8 InBrushIndex) const +{ + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return nullptr; + } + + if (InTargetType == ELandscapeToolTargetType::Type::Heightmap) + { + if (Layer->HeightmapBrushOrderIndices.IsValidIndex(InBrushIndex)) + { + int8 ActualBrushIndex = Layer->HeightmapBrushOrderIndices[InBrushIndex]; + if (Layer->Brushes.IsValidIndex(ActualBrushIndex)) + { + return Layer->Brushes[ActualBrushIndex].BPCustomBrush; + } + } + } + else + { + if (Layer->WeightmapBrushOrderIndices.IsValidIndex(InBrushIndex)) + { + int8 ActualBrushIndex = Layer->WeightmapBrushOrderIndices[InBrushIndex]; + if (Layer->Brushes.IsValidIndex(ActualBrushIndex)) + { + return Layer->Brushes[ActualBrushIndex].BPCustomBrush; + } + } + } + + return nullptr; +} + +TArray FEdModeLandscape::GetBrushesForCurrentProceduralLayer(int32 InTargetType) +{ + TArray Brushes; + + FProceduralLayer* Layer = GetCurrentProceduralLayer(); + + if (Layer == nullptr) + { + return Brushes; + } + + Brushes.Reserve(Layer->Brushes.Num()); + + for (const FLandscapeProceduralLayerBrush& Brush : Layer->Brushes) + { + if ((Brush.BPCustomBrush->IsAffectingHeightmap() && InTargetType == ELandscapeToolTargetType::Type::Heightmap) + || (Brush.BPCustomBrush->IsAffectingWeightmap() && InTargetType == ELandscapeToolTargetType::Type::Weightmap)) + { + Brushes.Add(Brush.BPCustomBrush); + } + } + + return Brushes; +} + +FProceduralLayer* FEdModeLandscape::GetCurrentProceduralLayer() const +{ + if (!CurrentToolTarget.LandscapeInfo.IsValid()) + { + return nullptr; + } + + ALandscape* Landscape = CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + + if (Landscape == nullptr) + { + return nullptr; + } + + FName CurrentLayerName = GetCurrentProceduralLayerName(); + + if (CurrentLayerName == NAME_None) + { + return nullptr; + } + + for (FProceduralLayer& Layer : Landscape->ProceduralLayers) + { + if (Layer.Name == CurrentLayerName) + { + return &Layer; + } + } + + return nullptr; +} + +void FEdModeLandscape::ChangeHeightmapsToCurrentProceduralLayerHeightmaps(bool InResetCurrentEditingHeightmap) +{ + if (!CurrentToolTarget.LandscapeInfo.IsValid() || !CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + return; + } + + TArray AllLandscapes; + AllLandscapes.Add(CurrentToolTarget.LandscapeInfo->LandscapeActor.Get()); + + for (const auto& It : CurrentToolTarget.LandscapeInfo->Proxies) + { + AllLandscapes.Add(It); + } + + FName CurrentLayerName = GetCurrentProceduralLayerName(); + + if (CurrentLayerName == NAME_None) + { + return; + } + + for (ALandscapeProxy* LandscapeProxy : AllLandscapes) + { + FProceduralLayerData* CurrentLayerData = LandscapeProxy->ProceduralLayersData.Find(CurrentLayerName); + + if (CurrentLayerData == nullptr) + { + continue; + } + + for (ULandscapeComponent* Component : LandscapeProxy->LandscapeComponents) + { + if (InResetCurrentEditingHeightmap) + { + Component->SetCurrentEditingHeightmap(nullptr); + } + else + { + UTexture2D** LayerHeightmap = CurrentLayerData->Heightmaps.Find(Component->GetHeightmap()); + + if (LayerHeightmap != nullptr) + { + Component->SetCurrentEditingHeightmap(*LayerHeightmap); + } + } + + Component->MarkRenderStateDirty(); + } + } +} + +void FEdModeLandscape::OnLevelActorAdded(AActor* InActor) +{ + ALandscapeBlueprintCustomBrush* Brush = Cast(InActor); + + if (Brush != nullptr && Brush->GetTypedOuter() != GetTransientPackage()) + { + AddBrushToCurrentProceduralLayer(CurrentToolTarget.TargetType, Brush); + RefreshDetailPanel(); + } +} + +void FEdModeLandscape::OnLevelActorRemoved(AActor* InActor) +{ + ALandscapeBlueprintCustomBrush* Brush = Cast(InActor); + + if (Brush != nullptr && Brush->GetTypedOuter() != GetTransientPackage()) + { + RemoveBrushFromCurrentProceduralLayer(CurrentToolTarget.TargetType, Brush); + RefreshDetailPanel(); + } +} + bool LandscapeEditorUtils::SetHeightmapData(ALandscapeProxy* Landscape, const TArray& Data) { FIntRect ComponentsRect = Landscape->GetBoundingRect() + Landscape->LandscapeSectionOffset; diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.h b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.h index 8d6c872b89e4..c0fc96bf8666 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.h +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdMode.h @@ -69,8 +69,9 @@ struct FLandscapeTargetListInfo TWeakObjectPtr ThumbnailMIC; // ignored for heightmap int32 DebugColorChannel; // ignored for heightmap uint32 bValid : 1; // ignored for heightmap + int32 ProceduralLayerIndex; - FLandscapeTargetListInfo(FText InTargetName, ELandscapeToolTargetType::Type InTargetType, const FLandscapeInfoLayerSettings& InLayerSettings) + FLandscapeTargetListInfo(FText InTargetName, ELandscapeToolTargetType::Type InTargetType, const FLandscapeInfoLayerSettings& InLayerSettings, int32 InProceduralLayerIndex) : TargetName(InTargetName) , TargetType(InTargetType) , LandscapeInfo(InLayerSettings.Owner->GetLandscapeInfo()) @@ -80,10 +81,11 @@ struct FLandscapeTargetListInfo , ThumbnailMIC(InLayerSettings.ThumbnailMIC) , DebugColorChannel(InLayerSettings.DebugColorChannel) , bValid(InLayerSettings.bValid) + , ProceduralLayerIndex (InProceduralLayerIndex) { } - FLandscapeTargetListInfo(FText InTargetName, ELandscapeToolTargetType::Type InTargetType, ULandscapeInfo* InLandscapeInfo) + FLandscapeTargetListInfo(FText InTargetName, ELandscapeToolTargetType::Type InTargetType, ULandscapeInfo* InLandscapeInfo, int32 InProceduralLayerIndex) : TargetName(InTargetName) , TargetType(InTargetType) , LandscapeInfo(InLandscapeInfo) @@ -92,6 +94,7 @@ struct FLandscapeTargetListInfo , Owner(NULL) , ThumbnailMIC(NULL) , bValid(true) + , ProceduralLayerIndex(InProceduralLayerIndex) { } @@ -307,6 +310,7 @@ public: void InitializeTool_Splines(); void InitializeTool_Ramp(); void InitializeTool_Mirror(); + void InitializeTool_BPCustom(); void InitializeToolModes(); /** Destructor */ @@ -466,6 +470,31 @@ public: void OnLandscapeMaterialChangedDelegate(); void RefreshDetailPanel(); + // Procedural Layers + int32 GetProceduralLayerCount() const; + void SetCurrentProceduralLayer(int32 InLayerIndex); + int32 GetCurrentProceduralLayerIndex() const; + FName GetCurrentProceduralLayerName() const; + FName GetProceduralLayerName(int32 InLayerIndex) const; + void SetProceduralLayerName(int32 InLayerIndex, const FName& InName); + float GetProceduralLayerWeight(int32 InLayerIndex) const; + void SetProceduralLayerWeight(float InWeight, int32 InLayerIndex); + void SetProceduralLayerVisibility(bool InVisible, int32 InLayerIndex); + bool IsProceduralLayerVisible(int32 InLayerIndex) const; + void AddBrushToCurrentProceduralLayer(int32 InTargetType, class ALandscapeBlueprintCustomBrush* InBrush); + void RemoveBrushFromCurrentProceduralLayer(int32 InTargetType, class ALandscapeBlueprintCustomBrush* InBrush); + bool AreAllBrushesCommitedToCurrentProceduralLayer(int32 InTargetType); + void SetCurrentProceduralLayerBrushesCommitState(int32 InTargetType, bool InCommited); + TArray& GetBrushesOrderForCurrentProceduralLayer(int32 InTargetType) const; + class ALandscapeBlueprintCustomBrush* GetBrushForCurrentProceduralLayer(int32 InTargetType, int8 BrushIndex) const; + TArray GetBrushesForCurrentProceduralLayer(int32 InTargetType); + struct FProceduralLayer* GetCurrentProceduralLayer() const; + void ChangeHeightmapsToCurrentProceduralLayerHeightmaps(bool InResetCurrentEditingHeightmap = false); + void RequestProceduralContentUpdate(); + + void OnLevelActorAdded(AActor* InActor); + void OnLevelActorRemoved(AActor* InActor); + DECLARE_EVENT(FEdModeLandscape, FTargetsListUpdated); static FTargetsListUpdated TargetsListUpdated; @@ -518,6 +547,9 @@ private: FDelegateHandle OnLevelsChangedDelegateHandle; FDelegateHandle OnMaterialCompilationFinishedDelegateHandle; + FDelegateHandle OnLevelActorDeletedDelegateHandle; + FDelegateHandle OnLevelActorAddedDelegateHandle; + /** Check if we are painting using the VREditor */ bool bIsPaintingInVR; diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeBPCustomTools.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeBPCustomTools.cpp new file mode 100644 index 000000000000..204aff20d1f9 --- /dev/null +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeBPCustomTools.cpp @@ -0,0 +1,207 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "CoreMinimal.h" +#include "InputCoreTypes.h" +#include "Materials/MaterialInterface.h" +#include "AI/NavigationSystemBase.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "UnrealWidget.h" +#include "EditorModeManager.h" +#include "EditorViewportClient.h" +#include "LandscapeToolInterface.h" +#include "LandscapeProxy.h" +#include "LandscapeEdMode.h" +#include "Containers/ArrayView.h" +#include "LandscapeEditorObject.h" +#include "ScopedTransaction.h" +#include "LandscapeEdit.h" +#include "LandscapeDataAccess.h" +#include "LandscapeRender.h" +#include "LandscapeHeightfieldCollisionComponent.h" +#include "LandscapeEdModeTools.h" +#include "LandscapeBPCustomBrush.h" +#include "LandscapeInfo.h" +#include "Landscape.h" +//#include "LandscapeDataAccess.h" + +#define LOCTEXT_NAMESPACE "Landscape" + +template +class FLandscapeToolBPCustom : public FLandscapeTool +{ +protected: + FEdModeLandscape* EdMode; + +public: + FLandscapeToolBPCustom(FEdModeLandscape* InEdMode) + : EdMode(InEdMode) + { + } + + virtual bool UsesTransformWidget() const { return true; } + virtual bool OverrideWidgetLocation() const { return false; } + virtual bool OverrideWidgetRotation() const { return false; } + + virtual void AddReferencedObjects(FReferenceCollector& Collector) override + { + } + + virtual const TCHAR* GetToolName() override { return TEXT("BPCustom"); } + virtual FText GetDisplayName() override { return FText(); }; + + virtual void SetEditRenderType() override { GLandscapeEditRenderMode = ELandscapeEditRenderMode::None | (GLandscapeEditRenderMode & ELandscapeEditRenderMode::BitMaskForMask); } + virtual bool SupportsMask() override { return false; } + + virtual ELandscapeToolTargetTypeMask::Type GetSupportedTargetTypes() override + { + return ELandscapeToolTargetTypeMask::FromType(ToolTarget::TargetType); + } + + virtual void EnterTool() override + { + } + + virtual void ExitTool() override + { + } + + virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) + { + } + + virtual bool BeginTool(FEditorViewportClient* ViewportClient, const FLandscapeToolTarget& Target, const FVector& InHitLocation) override + { + if (EdMode->UISettings->BlueprintCustomBrush == nullptr) + { + return false; + } + + ALandscapeBlueprintCustomBrush* DefaultObject = Cast(EdMode->UISettings->BlueprintCustomBrush->GetDefaultObject(false)); + + if (DefaultObject == nullptr) + { + return false; + } + + // Only allow placing brushes that would affect our target type + if ((DefaultObject->IsAffectingHeightmap() && Target.TargetType == ELandscapeToolTargetType::Heightmap) || (DefaultObject->IsAffectingWeightmap() && Target.TargetType == ELandscapeToolTargetType::Weightmap)) + { + ULandscapeInfo* Info = EdMode->CurrentToolTarget.LandscapeInfo.Get(); + check(Info); + + FVector SpawnLocation = Info->GetLandscapeProxy()->LandscapeActorToWorld().TransformPosition(InHitLocation); + + FActorSpawnParameters SpawnInfo; + SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnInfo.bNoFail = true; + SpawnInfo.OverrideLevel = Info->LandscapeActor.Get()->GetTypedOuter(); // always spawn in the same level as the one containing the ALandscape + + ALandscapeBlueprintCustomBrush* Brush = ViewportClient->GetWorld()->SpawnActor(EdMode->UISettings->BlueprintCustomBrush, SpawnLocation, FRotator(0.0f), SpawnInfo); + EdMode->UISettings->BlueprintCustomBrush = nullptr; + + GEditor->SelectNone(true, true); + GEditor->SelectActor(Brush, true, true); + + EdMode->RefreshDetailPanel(); + } + + return true; + } + + virtual void EndTool(FEditorViewportClient* ViewportClient) override + { + } + + virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) override + { + return false; + } + + virtual bool InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent) override + { + if (InKey == EKeys::Enter && InEvent == IE_Pressed) + { + } + + return false; + } + + virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override + { + return false; + } + + virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override + { + // The editor can try to render the tool before the UpdateLandscapeEditorData command runs and the landscape editor realizes that the landscape has been hidden/deleted + const ULandscapeInfo* const LandscapeInfo = EdMode->CurrentToolTarget.LandscapeInfo.Get(); + const ALandscapeProxy* const LandscapeProxy = LandscapeInfo->GetLandscapeProxy(); + if (LandscapeProxy) + { + const FTransform LandscapeToWorld = LandscapeProxy->LandscapeActorToWorld(); + + int32 MinX, MinY, MaxX, MaxY; + if (LandscapeInfo->GetLandscapeExtent(MinX, MinY, MaxX, MaxY)) + { + // TODO if required + } + } + } + + +protected: +/* float GetLocalZAtPoint(const ULandscapeInfo* LandscapeInfo, int32 x, int32 y) const + { + // try to find Z location + TSet Components; + LandscapeInfo->GetComponentsInRegion(x, y, x, y, Components); + for (ULandscapeComponent* Component : Components) + { + FLandscapeComponentDataInterface DataInterface(Component); + return LandscapeDataAccess::GetLocalHeight(DataInterface.GetHeight(x - Component->SectionBaseX, y - Component->SectionBaseY)); + } + return 0.0f; + } +*/ + +public: +}; +/* +void FEdModeLandscape::ApplyMirrorTool() +{ + if (CurrentTool->GetToolName() == FName("Mirror")) + { + FLandscapeToolMirror* MirrorTool = (FLandscapeToolMirror*)CurrentTool; + MirrorTool->ApplyMirror(); + GEditor->RedrawLevelEditingViewports(); + } +} + +void FEdModeLandscape::CenterMirrorTool() +{ + if (CurrentTool->GetToolName() == FName("Mirror")) + { + FLandscapeToolMirror* MirrorTool = (FLandscapeToolMirror*)CurrentTool; + MirrorTool->CenterMirrorPoint(); + GEditor->RedrawLevelEditingViewports(); + } +} +*/ + + + +// +// Toolset initialization +// +void FEdModeLandscape::InitializeTool_BPCustom() +{ + auto Sculpt_Tool_BPCustom = MakeUnique>(this); + Sculpt_Tool_BPCustom->ValidBrushes.Add("BrushSet_Dummy"); + LandscapeTools.Add(MoveTemp(Sculpt_Tool_BPCustom)); + + auto Paint_Tool_BPCustom = MakeUnique>(this); + Paint_Tool_BPCustom->ValidBrushes.Add("BrushSet_Dummy"); + LandscapeTools.Add(MoveTemp(Paint_Tool_BPCustom)); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeComponentTools.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeComponentTools.cpp index 330970ff80fb..273649a39d52 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeComponentTools.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeComponentTools.cpp @@ -22,6 +22,7 @@ #include "PhysicalMaterials/PhysicalMaterial.h" #include "Materials/MaterialExpressionLandscapeVisibilityMask.h" #include "Algo/Copy.h" +#include "Settings/EditorExperimentalSettings.h" #define LOCTEXT_NAMESPACE "Landscape" @@ -214,6 +215,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + Cache.SetCachedData(X1, Y1, X2, Y2, Data); Cache.Flush(); } @@ -293,6 +302,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + Cache.SetCachedData(X1, Y1, X2, Y2, Data); Cache.Flush(); } @@ -493,7 +510,7 @@ public: for (ULandscapeComponent* Component : TargetSelectedComponents) { Component->Modify(); - OldHeightmapTextures.Add(Component->HeightmapTexture); + OldHeightmapTextures.Add(Component->GetHeightmap()); } // Need to split all the component which share Heightmap with selected components @@ -502,8 +519,8 @@ public: for (ULandscapeComponent* Component : TargetSelectedComponents) { // Search neighbor only - const int32 SearchX = Component->HeightmapTexture->Source.GetSizeX() / NeedHeightmapSize - 1; - const int32 SearchY = Component->HeightmapTexture->Source.GetSizeY() / NeedHeightmapSize - 1; + const int32 SearchX = Component->GetHeightmap()->Source.GetSizeX() / NeedHeightmapSize - 1; + const int32 SearchY = Component->GetHeightmap()->Source.GetSizeY() / NeedHeightmapSize - 1; const FIntPoint ComponentBase = Component->GetSectionBase() / Component->ComponentSizeQuads; for (int32 Y = -SearchY; Y <= SearchY; ++Y) @@ -511,7 +528,7 @@ public: for (int32 X = -SearchX; X <= SearchX; ++X) { ULandscapeComponent* const Neighbor = LandscapeInfo->XYtoComponentMap.FindRef(ComponentBase + FIntPoint(X, Y)); - if (Neighbor && Neighbor->HeightmapTexture == Component->HeightmapTexture && !HeightmapUpdateComponents.Contains(Neighbor)) + if (Neighbor && Neighbor->GetHeightmap() == Component->GetHeightmap() && !HeightmapUpdateComponents.Contains(Neighbor)) { Neighbor->Modify(); bool bNeedsMoveToCurrentLevel = TargetSelectedComponents.Contains(Neighbor); @@ -895,6 +912,12 @@ public: NewComponents[Idx]->RegisterComponent(); } + if (LandscapeInfo->LandscapeActor.Get()->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + if (bHasXYOffset) { XYOffsetCache.SetCachedData(X1, Y1, X2, Y2, XYOffsetData); @@ -1711,6 +1734,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + if (bApplyToAll) { HeightCache.SetCachedData(X1, Y1, X2, Y2, HeightData); diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeErosionTools.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeErosionTools.cpp index 8f85cc1e9088..19f56c93f847 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeErosionTools.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeErosionTools.cpp @@ -6,7 +6,13 @@ #include "LandscapeEdMode.h" #include "LandscapeEditorObject.h" #include "LandscapeEdModeTools.h" +#include "Landscape.h" +#include "Logging/TokenizedMessage.h" +#include "Logging/MessageLog.h" +#include "Misc/MapErrors.h" +#include "Settings/EditorExperimentalSettings.h" +#define LOCTEXT_NAMESPACE "LandscapeTools" // // FLandscapeToolErosionBase // @@ -251,6 +257,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + HeightCache.SetCachedData(X1, Y1, X2, Y2, HeightData); HeightCache.Flush(); if (bWeightApplied) @@ -474,6 +488,14 @@ public: LowPassFilter(X1, Y1, X2, Y2, BrushInfo, HeightData, UISettings->HErosionDetailScale, 1.0f); } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + HeightCache.SetCachedData(X1, Y1, X2, Y2, HeightData); HeightCache.Flush(); } @@ -512,3 +534,5 @@ void FEdModeLandscape::InitializeTool_HydraErosion() Tool_HydraErosion->ValidBrushes.Add("BrushSet_Pattern"); LandscapeTools.Add(MoveTemp(Tool_HydraErosion)); } + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModePaintTools.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModePaintTools.cpp index 06efff70dbcf..30b9edfbbd65 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModePaintTools.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModePaintTools.cpp @@ -13,7 +13,13 @@ #include "LandscapeEdit.h" #include "LandscapeDataAccess.h" #include "LandscapeEdModeTools.h" +#include "Settings/EditorExperimentalSettings.h" +#include "Landscape.h" +#include "Logging/TokenizedMessage.h" +#include "Logging/MessageLog.h" +#include "Misc/MapErrors.h" +#define LOCTEXT_NAMESPACE "LandscapeTools" const int32 FNoiseParameter::Permutations[256] = { @@ -52,6 +58,62 @@ public: { return ELandscapeToolTargetTypeMask::FromType(TToolTarget::TargetType); } + + virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override + { + FLandscapeToolBase::Tick(ViewportClient, DeltaTime); + + if (GetMutableDefault()->bProceduralLandscape && this->IsToolActive()) + { + ALandscape* Landscape = this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + if (Landscape != nullptr) + { + Landscape->RequestProceduralContentUpdate(this->EdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap ? EProceduralContentUpdateFlag::Heightmap_Render : EProceduralContentUpdateFlag::Weightmap_Render); + } + } + } + + virtual bool BeginTool(FEditorViewportClient* ViewportClient, const FLandscapeToolTarget& InTarget, const FVector& InHitLocation) override + { + if (GetMutableDefault()->bProceduralLandscape) + { + ALandscape* Landscape = this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor.Get(); + if (Landscape != nullptr) + { + Landscape->RequestProceduralContentUpdate(this->EdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap ? EProceduralContentUpdateFlag::Heightmap_Render : EProceduralContentUpdateFlag::Weightmap_Render); + } + + this->EdMode->ChangeHeightmapsToCurrentProceduralLayerHeightmaps(false); + } + + return FLandscapeToolBase::BeginTool(ViewportClient, InTarget, InHitLocation); + } + + virtual void EndTool(FEditorViewportClient* ViewportClient) override + { + if (GetMutableDefault()->bProceduralLandscape) + { + if (this->EdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Type::Heightmap) + { + this->EdMode->ChangeHeightmapsToCurrentProceduralLayerHeightmaps(true); + + if (this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::Heightmap_All); + } + } + else + { + // TODO: Activate/Deactivate weightmap layers + if (this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor.IsValid()) + { + this->EdMode->CurrentToolTarget.LandscapeInfo->LandscapeActor->RequestProceduralContentUpdate(EProceduralContentUpdateFlag::Weightmap_All); + } + } + } + + FLandscapeToolBase::EndTool(ViewportClient); + } }; @@ -249,6 +311,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + this->Cache.SetCachedData(X1, Y1, X2, Y2, Data, UISettings->PaintingRestriction); this->Cache.Flush(); } @@ -484,6 +554,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + this->Cache.SetCachedData(X1, Y1, X2, Y2, Data); this->Cache.Flush(); } @@ -610,6 +688,14 @@ public: } } + ALandscape* Landscape = this->LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + this->Cache.SetCachedData(X1, Y1, X2, Y2, Data, UISettings->PaintingRestriction); this->Cache.Flush(); } @@ -792,6 +878,14 @@ public: } } + ALandscape* Landscape = this->LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + this->Cache.SetCachedData(X1, Y1, X2, Y2, Data, UISettings->PaintingRestriction); this->Cache.Flush(); } @@ -1014,6 +1108,14 @@ public: } } + ALandscape* Landscape = this->LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + this->Cache.SetCachedData(X1, Y1, X2, Y2, Data, UISettings->PaintingRestriction); this->Cache.Flush(); } @@ -1097,3 +1199,5 @@ void FEdModeLandscape::InitializeTool_Noise() Tool_Noise_Weightmap->ValidBrushes.Add("BrushSet_Pattern"); LandscapeTools.Add(MoveTemp(Tool_Noise_Weightmap)); } + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeXYOffset.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeXYOffset.cpp index f1f657f05332..d4f0572eb6fb 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeXYOffset.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEdModeXYOffset.cpp @@ -7,7 +7,12 @@ #include "Landscape.h" #include "LandscapeDataAccess.h" #include "LandscapeEdModeTools.h" +#include "Logging/TokenizedMessage.h" +#include "Logging/MessageLog.h" +#include "Misc/MapErrors.h" +#include "Settings/EditorExperimentalSettings.h" +#define LOCTEXT_NAMESPACE "LandscapeTools" namespace { FORCEINLINE FVector4 GetWorldPos(const FMatrix& LocalToWorld, FVector2D LocalXY, uint16 Height, FVector2D XYOffset) @@ -536,6 +541,14 @@ public: } } + ALandscape* Landscape = LandscapeInfo->LandscapeActor.Get(); + + if (Landscape->HasProceduralContent && !GetMutableDefault()->bProceduralLandscape) + { + FMessageLog("MapCheck").Warning()->AddToken(FTextToken::Create(LOCTEXT("LandscapeProcedural_ChangingDataWithoutSettings", "This map contains landscape procedural content, modifying the landscape data will result in data loss when the map is reopened with Landscape Procedural settings on. Please enable Landscape Procedural settings before modifying the data."))); + FMessageLog("MapCheck").Open(EMessageSeverity::Warning); + } + // Apply to XYOffset Texture map and Height map Cache.SetCachedData(X1, Y1, X2, Y2, XYOffsetVectorData); Cache.Flush(); @@ -571,3 +584,5 @@ void FEdModeLandscape::InitializeTool_Retopologize() Tool_Retopologize->ValidBrushes.Add("BrushSet_Pattern"); LandscapeTools.Add(MoveTemp(Tool_Retopologize)); } + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.cpp index c47ea2b34abb..c30b11410036 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.cpp @@ -54,6 +54,9 @@ void FLandscapeEditorCommands::RegisterCommands() UI_COMMAND(VisibilityTool, "Tool - Visibility", "", EUserInterfaceActionType::RadioButton, FInputChord()); NameToCommandMap.Add("Tool_Visibility", VisibilityTool); + UI_COMMAND(BPCustomTool, "Tool - BP Custom", "", EUserInterfaceActionType::RadioButton, FInputChord()); + NameToCommandMap.Add("Tool_BPCustom", BPCustomTool); + UI_COMMAND(SelectComponentTool, "Tool - Component Selection", "", EUserInterfaceActionType::RadioButton, FInputChord()); NameToCommandMap.Add("Tool_Select", SelectComponentTool); UI_COMMAND(AddComponentTool, "Tool - Add Components", "", EUserInterfaceActionType::RadioButton, FInputChord()); diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.h b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.h index 3e9ead6a8a2a..0f01406066b0 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.h +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorCommands.h @@ -43,6 +43,7 @@ public: TSharedPtr NoiseTool; TSharedPtr RetopologizeTool; TSharedPtr VisibilityTool; + TSharedPtr BPCustomTool; TSharedPtr SelectComponentTool; TSharedPtr AddComponentTool; diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_NewLandscape.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_NewLandscape.cpp index a1d96a9f92d2..22b2fbe54a0f 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_NewLandscape.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_NewLandscape.cpp @@ -859,6 +859,7 @@ FReply FLandscapeEditorDetailCustomization_NewLandscape::OnCreateButtonClicked() LandscapeEdMode->SetCurrentTool("Select"); // change tool so switching back to the manage mode doesn't give "New Landscape" again LandscapeEdMode->SetCurrentTool("Sculpt"); // change to sculpting mode and tool + LandscapeEdMode->SetCurrentProceduralLayer(0); if (LandscapeEdMode->CurrentToolTarget.LandscapeInfo.IsValid()) { diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.cpp new file mode 100644 index 000000000000..acd1bb256fa5 --- /dev/null +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.cpp @@ -0,0 +1,404 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "LandscapeEditorDetailCustomization_ProceduralBrushStack.h" +#include "IDetailChildrenBuilder.h" +#include "Framework/Commands/UIAction.h" +#include "Widgets/Text/STextBlock.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Misc/MessageDialog.h" +#include "Modules/ModuleManager.h" +#include "Brushes/SlateColorBrush.h" +#include "Layout/WidgetPath.h" +#include "SlateOptMacros.h" +#include "Framework/Application/MenuStack.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SButton.h" +#include "EditorModeManager.h" +#include "EditorModes.h" +#include "DetailLayoutBuilder.h" +#include "IDetailPropertyRow.h" +#include "DetailCategoryBuilder.h" +#include "PropertyCustomizationHelpers.h" + +#include "ScopedTransaction.h" + +#include "LandscapeEditorDetailCustomization_TargetLayers.h" +#include "Widgets/Input/SEditableText.h" +#include "LandscapeBPCustomBrush.h" + +#define LOCTEXT_NAMESPACE "LandscapeEditor.Layers" + +TSharedRef FLandscapeEditorDetailCustomization_ProceduralBrushStack::MakeInstance() +{ + return MakeShareable(new FLandscapeEditorDetailCustomization_ProceduralBrushStack); +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void FLandscapeEditorDetailCustomization_ProceduralBrushStack::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + IDetailCategoryBuilder& LayerCategory = DetailBuilder.EditCategory("Current Layer Brushes"); + + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode && LandscapeEdMode->CurrentToolMode != nullptr) + { + const FName CurrentToolName = LandscapeEdMode->CurrentTool->GetToolName(); + + if (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes != 0 && CurrentToolName == TEXT("BPCustom")) + { + LayerCategory.AddCustomBuilder(MakeShareable(new FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack(DetailBuilder.GetThumbnailPool().ToSharedRef()))); + } + } +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +////////////////////////////////////////////////////////////////////////// + +FEdModeLandscape* FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetEditorMode() +{ + return (FEdModeLandscape*)GLevelEditorModeTools().GetActiveMode(FBuiltinEditorModes::EM_Landscape); +} + +FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack(TSharedRef InThumbnailPool) + : ThumbnailPool(InThumbnailPool) +{ +} + +FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::~FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack() +{ + +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::SetOnRebuildChildren(FSimpleDelegate InOnRegenerateChildren) +{ +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode == NULL) + { + return; + } + + NodeRow.NameWidget + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(FText::FromString(TEXT("Stack"))) + ]; +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode != NULL) + { + TSharedPtr BrushesList = SNew(SDragAndDropVerticalBox) + .OnCanAcceptDrop(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleCanAcceptDrop) + .OnAcceptDrop(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleAcceptDrop) + .OnDragDetected(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleDragDetected); + + BrushesList->SetDropIndicator_Above(*FEditorStyle::GetBrush("LandscapeEditor.TargetList.DropZone.Above")); + BrushesList->SetDropIndicator_Below(*FEditorStyle::GetBrush("LandscapeEditor.TargetList.DropZone.Below")); + + ChildrenBuilder.AddCustomRow(FText::FromString(FString(TEXT("Brush Stack")))) + .Visibility(EVisibility::Visible) + [ + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .Padding(0, 2) + [ + BrushesList.ToSharedRef() + ] + + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .Padding(0, 2) + [ + SNew(SHorizontalBox) + + +SHorizontalBox::Slot() + .HAlign(HAlign_Right) + //.Padding(4, 0) + [ + SNew(SButton) + .Text(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetCommitBrushesButtonText) + .OnClicked(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::ToggleCommitBrushes) + .IsEnabled(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::IsCommitBrushesButtonEnabled) + ] + ] + ]; + + if (LandscapeEdMode->CurrentToolMode != nullptr) + { + const TArray& BrushOrderStack = LandscapeEdMode->GetBrushesOrderForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + for (int32 i = 0; i < BrushOrderStack.Num(); ++i) + { + TSharedPtr GeneratedRowWidget = GenerateRow(i); + + if (GeneratedRowWidget.IsValid()) + { + BrushesList->AddSlot() + .AutoHeight() + [ + GeneratedRowWidget.ToSharedRef() + ]; + } + } + } + } +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +TSharedPtr FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GenerateRow(int32 InBrushIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + TSharedPtr RowWidget = SNew(SLandscapeEditorSelectableBorder) + .Padding(0) + .VAlign(VAlign_Center) + .OnSelected(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::OnBrushSelectionChanged, InBrushIndex) + .IsSelected(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::IsBrushSelected, InBrushIndex))) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .Padding(0, 2) + [ + SNew(STextBlock) + .ColorAndOpacity(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetBrushTextColor, InBrushIndex))) + .Text(this, &FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetBrushText, InBrushIndex) + ] + ] + ]; + + return RowWidget; +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +bool FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::IsBrushSelected(int32 InBrushIndex) const +{ + ALandscapeBlueprintCustomBrush* Brush = GetBrush(InBrushIndex); + + return Brush != nullptr ? Brush->IsSelected() : false; +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::OnBrushSelectionChanged(int32 InBrushIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr && LandscapeEdMode->AreAllBrushesCommitedToCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType)) + { + return; + } + + ALandscapeBlueprintCustomBrush* Brush = GetBrush(InBrushIndex); + + if (Brush != nullptr && !Brush->IsCommited()) + { + GEditor->SelectNone(true, true); + GEditor->SelectActor(Brush, true, true); + } +} + +FText FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetBrushText(int32 InBrushIndex) const +{ + ALandscapeBlueprintCustomBrush* Brush = GetBrush(InBrushIndex); + + if (Brush != nullptr) + { + return FText::FromString(Brush->GetActorLabel()); + } + + return FText::FromName(NAME_None); +} + +FSlateColor FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetBrushTextColor(int32 InBrushIndex) const +{ + ALandscapeBlueprintCustomBrush* Brush = GetBrush(InBrushIndex); + + if (Brush != nullptr) + { + return Brush->IsCommited() ? FSlateColor::UseSubduedForeground() : FSlateColor::UseForeground(); + } + + return FSlateColor::UseSubduedForeground(); +} + +ALandscapeBlueprintCustomBrush* FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetBrush(int32 InBrushIndex) const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + return LandscapeEdMode->GetBrushForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType, InBrushIndex); + } + + return nullptr; +} + +FReply FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::ToggleCommitBrushes() +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + bool CommitBrushes = !LandscapeEdMode->AreAllBrushesCommitedToCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + if (CommitBrushes) + { + TArray BrushStack = LandscapeEdMode->GetBrushesForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + for (ALandscapeBlueprintCustomBrush* Brush : BrushStack) + { + GEditor->SelectActor(Brush, false, true); + } + } + + LandscapeEdMode->SetCurrentProceduralLayerBrushesCommitState(LandscapeEdMode->CurrentToolTarget.TargetType, CommitBrushes); + } + + return FReply::Handled(); +} + +bool FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::IsCommitBrushesButtonEnabled() const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + TArray BrushStack = LandscapeEdMode->GetBrushesForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + return BrushStack.Num() > 0; + } + + return false; +} + +FText FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::GetCommitBrushesButtonText() const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + return LandscapeEdMode->AreAllBrushesCommitedToCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType) ? LOCTEXT("UnCommitBrushesText", "Uncommit") : LOCTEXT("CommitBrushesText", "Commit"); + } + + return FText::FromName(NAME_None); +} + +FReply FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, int32 SlotIndex, SVerticalBox::FSlot* Slot) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + const TArray& BrushOrderStack = LandscapeEdMode->GetBrushesOrderForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + if (BrushOrderStack.IsValidIndex(SlotIndex)) + { + TSharedPtr Row = GenerateRow(SlotIndex); + + if (Row.IsValid()) + { + return FReply::Handled().BeginDragDrop(FLandscapeBrushDragDropOp::New(SlotIndex, Slot, Row)); + } + } + } + + return FReply::Unhandled(); +} + +TOptional FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleCanAcceptDrop(const FDragDropEvent& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, SVerticalBox::FSlot* Slot) +{ + TSharedPtr DragDropOperation = DragDropEvent.GetOperationAs(); + + if (DragDropOperation.IsValid()) + { + return DropZone; + } + + return TOptional(); +} + +FReply FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack::HandleAcceptDrop(FDragDropEvent const& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, int32 SlotIndex, SVerticalBox::FSlot* Slot) +{ + TSharedPtr DragDropOperation = DragDropEvent.GetOperationAs(); + + if (DragDropOperation.IsValid()) + { + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + TArray& BrushOrderStack = LandscapeEdMode->GetBrushesOrderForCurrentProceduralLayer(LandscapeEdMode->CurrentToolTarget.TargetType); + + if (BrushOrderStack.IsValidIndex(DragDropOperation->SlotIndexBeingDragged) && BrushOrderStack.IsValidIndex(SlotIndex)) + { + int32 StartingLayerIndex = DragDropOperation->SlotIndexBeingDragged; + int32 DestinationLayerIndex = SlotIndex; + + if (StartingLayerIndex != INDEX_NONE && DestinationLayerIndex != INDEX_NONE) + { + int8 MovingBrushIndex = BrushOrderStack[StartingLayerIndex]; + + BrushOrderStack.RemoveAt(StartingLayerIndex); + BrushOrderStack.Insert(MovingBrushIndex, DestinationLayerIndex); + + LandscapeEdMode->RefreshDetailPanel(); + LandscapeEdMode->RequestProceduralContentUpdate(); + + return FReply::Handled(); + } + } + } + } + + return FReply::Unhandled(); +} + +TSharedRef FLandscapeBrushDragDropOp::New(int32 InSlotIndexBeingDragged, SVerticalBox::FSlot* InSlotBeingDragged, TSharedPtr WidgetToShow) +{ + TSharedRef Operation = MakeShareable(new FLandscapeBrushDragDropOp); + + Operation->MouseCursor = EMouseCursor::GrabHandClosed; + Operation->SlotIndexBeingDragged = InSlotIndexBeingDragged; + Operation->SlotBeingDragged = InSlotBeingDragged; + Operation->WidgetToShow = WidgetToShow; + + Operation->Construct(); + + return Operation; +} + +FLandscapeBrushDragDropOp::~FLandscapeBrushDragDropOp() +{ +} + +TSharedPtr FLandscapeBrushDragDropOp::GetDefaultDecorator() const +{ + return SNew(SBorder) + .BorderImage(FEditorStyle::GetBrush("ContentBrowser.AssetDragDropTooltipBackground")) + .Content() + [ + WidgetToShow.ToSharedRef() + ]; + +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.h b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.h new file mode 100644 index 000000000000..1b500fa34824 --- /dev/null +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralBrushStack.h @@ -0,0 +1,90 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Attribute.h" +#include "Layout/Visibility.h" +#include "Layout/Margin.h" +#include "Styling/SlateColor.h" +#include "Input/Reply.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SWidget.h" +#include "Widgets/Layout/SBorder.h" +#include "Editor/LandscapeEditor/Private/LandscapeEdMode.h" +#include "IDetailCustomNodeBuilder.h" +#include "IDetailCustomization.h" +#include "AssetThumbnail.h" +#include "Framework/SlateDelegates.h" +#include "Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_Base.h" + +class FDetailWidgetRow; +class IDetailChildrenBuilder; +class IDetailLayoutBuilder; +class SDragAndDropVerticalBox; + +/** + * Slate widgets customizer for the layers list in the Landscape Editor + */ + +class FLandscapeEditorDetailCustomization_ProceduralBrushStack : public FLandscapeEditorDetailCustomization_Base +{ +public: + /** Makes a new instance of this detail layout class for a specific detail view requesting it */ + static TSharedRef MakeInstance(); + + /** IDetailCustomization interface */ + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; + +class FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack : public IDetailCustomNodeBuilder, public TSharedFromThis +{ +public: + FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack(TSharedRef ThumbnailPool); + ~FLandscapeEditorCustomNodeBuilder_ProceduralBrushStack(); + + virtual void SetOnRebuildChildren( FSimpleDelegate InOnRegenerateChildren ) override; + virtual void GenerateHeaderRowContent( FDetailWidgetRow& NodeRow ) override; + virtual void GenerateChildContent( IDetailChildrenBuilder& ChildrenBuilder ) override; + virtual void Tick( float DeltaTime ) override {} + virtual bool RequiresTick() const override { return false; } + virtual bool InitiallyCollapsed() const override { return false; } + virtual FName GetName() const override { return "Brush Stack"; } + +protected: + TSharedRef ThumbnailPool; + + static class FEdModeLandscape* GetEditorMode(); + + TSharedPtr GenerateRow(int32 InLayerIndex); + + bool IsBrushSelected(int32 InBrushIndex) const; + void OnBrushSelectionChanged(int32 InBrushIndex); + FText GetBrushText(int32 InBrushIndex) const; + FSlateColor GetBrushTextColor(int32 InBrushIndex) const; + ALandscapeBlueprintCustomBrush* GetBrush(int32 InBrushIndex) const; + + FReply ToggleCommitBrushes(); + bool IsCommitBrushesButtonEnabled() const; + FText GetCommitBrushesButtonText() const; + + // Drag/Drop handling + FReply HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, int32 SlotIndex, SVerticalBox::FSlot* Slot); + TOptional HandleCanAcceptDrop(const FDragDropEvent& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, SVerticalBox::FSlot* Slot); + FReply HandleAcceptDrop(FDragDropEvent const& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, int32 SlotIndex, SVerticalBox::FSlot* Slot); +}; + +class FLandscapeBrushDragDropOp : public FDragAndDropVerticalBoxOp +{ +public: + DRAG_DROP_OPERATOR_TYPE(FLandscapeBrushDragDropOp, FDragAndDropVerticalBoxOp) + + TSharedPtr WidgetToShow; + + static TSharedRef New(int32 InSlotIndexBeingDragged, SVerticalBox::FSlot* InSlotBeingDragged, TSharedPtr InWidgetToShow); + +public: + virtual ~FLandscapeBrushDragDropOp(); + + virtual TSharedPtr GetDefaultDecorator() const override; +}; diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.cpp new file mode 100644 index 000000000000..4077ba4fa281 --- /dev/null +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.cpp @@ -0,0 +1,349 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "LandscapeEditorDetailCustomization_ProceduralLayers.h" +#include "IDetailChildrenBuilder.h" +#include "Framework/Commands/UIAction.h" +#include "Widgets/Text/STextBlock.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Misc/MessageDialog.h" +#include "Modules/ModuleManager.h" +#include "Brushes/SlateColorBrush.h" +#include "Layout/WidgetPath.h" +#include "SlateOptMacros.h" +#include "Framework/Application/MenuStack.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Notifications/SErrorText.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "EditorModeManager.h" +#include "EditorModes.h" +#include "LandscapeEditorModule.h" +#include "LandscapeEditorObject.h" + +#include "DetailLayoutBuilder.h" +#include "IDetailPropertyRow.h" +#include "DetailCategoryBuilder.h" +#include "PropertyCustomizationHelpers.h" + +#include "SLandscapeEditor.h" +#include "Dialogs/DlgPickAssetPath.h" +#include "ObjectTools.h" +#include "ScopedTransaction.h" +#include "DesktopPlatformModule.h" +#include "AssetRegistryModule.h" + +#include "LandscapeRender.h" +#include "Materials/MaterialExpressionLandscapeVisibilityMask.h" +#include "LandscapeEdit.h" +#include "IDetailGroup.h" +#include "Widgets/SBoxPanel.h" +#include "Editor/EditorStyle/Private/SlateEditorStyle.h" +#include "LandscapeEditorDetailCustomization_TargetLayers.h" +#include "Widgets/Input/SEditableText.h" +#include "Widgets/Input/SNumericEntryBox.h" + +#define LOCTEXT_NAMESPACE "LandscapeEditor.Layers" + +TSharedRef FLandscapeEditorDetailCustomization_ProceduralLayers::MakeInstance() +{ + return MakeShareable(new FLandscapeEditorDetailCustomization_ProceduralLayers); +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void FLandscapeEditorDetailCustomization_ProceduralLayers::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + IDetailCategoryBuilder& LayerCategory = DetailBuilder.EditCategory("Procedural Layers"); + + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode && LandscapeEdMode->CurrentToolMode != nullptr) + { + const FName CurrentToolName = LandscapeEdMode->CurrentTool->GetToolName(); + + if (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes != 0) + { + LayerCategory.AddCustomBuilder(MakeShareable(new FLandscapeEditorCustomNodeBuilder_ProceduralLayers(DetailBuilder.GetThumbnailPool().ToSharedRef()))); + } + } +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +////////////////////////////////////////////////////////////////////////// + +FEdModeLandscape* FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GetEditorMode() +{ + return (FEdModeLandscape*)GLevelEditorModeTools().GetActiveMode(FBuiltinEditorModes::EM_Landscape); +} + +FLandscapeEditorCustomNodeBuilder_ProceduralLayers::FLandscapeEditorCustomNodeBuilder_ProceduralLayers(TSharedRef InThumbnailPool) + : ThumbnailPool(InThumbnailPool) +{ +} + +FLandscapeEditorCustomNodeBuilder_ProceduralLayers::~FLandscapeEditorCustomNodeBuilder_ProceduralLayers() +{ +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::SetOnRebuildChildren(FSimpleDelegate InOnRegenerateChildren) +{ +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode == NULL) + { + return; + } + + NodeRow.NameWidget + [ + SNew(STextBlock) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .Text(FText::FromString(TEXT(""))) + ]; +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode != NULL) + { + TSharedPtr LayerList = SNew(SDragAndDropVerticalBox) + .OnCanAcceptDrop(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleCanAcceptDrop) + .OnAcceptDrop(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleAcceptDrop) + .OnDragDetected(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleDragDetected); + + LayerList->SetDropIndicator_Above(*FEditorStyle::GetBrush("LandscapeEditor.TargetList.DropZone.Above")); + LayerList->SetDropIndicator_Below(*FEditorStyle::GetBrush("LandscapeEditor.TargetList.DropZone.Below")); + + ChildrenBuilder.AddCustomRow(FText::FromString(FString(TEXT("Procedural Layers")))) + .Visibility(EVisibility::Visible) + [ + LayerList.ToSharedRef() + ]; + + for (int32 i = 0; i < LandscapeEdMode->GetProceduralLayerCount(); ++i) + { + TSharedPtr GeneratedRowWidget = GenerateRow(i); + + if (GeneratedRowWidget.IsValid()) + { + LayerList->AddSlot() + .AutoHeight() + [ + GeneratedRowWidget.ToSharedRef() + ]; + } + } + } +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION +TSharedPtr FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GenerateRow(int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + TSharedPtr RowWidget = SNew(SLandscapeEditorSelectableBorder) + .Padding(0) + .VAlign(VAlign_Center) + //.OnContextMenuOpening_Static(&FLandscapeEditorCustomNodeBuilder_Layers::OnTargetLayerContextMenuOpening, Target) + .OnSelected(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerSelectionChanged, InLayerIndex) + .IsSelected(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::IsLayerSelected, InLayerIndex))) + .Visibility(EVisibility::Visible) + [ + SNew(SHorizontalBox) + + /*+ SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(FMargin(2)) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush(TEXT("LandscapeEditor.Target_Heightmap"))) + ] + */ + + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(1.0f) + .Padding(4, 0) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .VAlign(VAlign_Center) + .Padding(0, 2) + .HAlign(HAlign_Left) + [ + SNew(SEditableText) + .SelectAllTextWhenFocused(true) + .IsReadOnly(true) + .Text(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GetLayerText, InLayerIndex) + .ToolTipText(LOCTEXT("FLandscapeEditorCustomNodeBuilder_ProceduralLayers_tooltip", "Name of the Layer")) + .OnTextCommitted(FOnTextCommitted::CreateSP(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerTextCommitted, InLayerIndex)) + ] + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + .Padding(0, 2) + .HAlign(HAlign_Center) + [ + SNew(SCheckBox) + .OnCheckStateChanged(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerVisibilityChanged, InLayerIndex) + .IsChecked(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::IsLayerVisible, InLayerIndex))) + .ToolTipText(LOCTEXT("FLandscapeEditorCustomNodeBuilder_ProceduralLayerVisibility_Tooltips", "Is layer visible")) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("FLandscapeEditorCustomNodeBuilder_ProceduralLayerVisibility", "Visibility")) + ] + ] + + SHorizontalBox::Slot() + .Padding(0) + .FillWidth(1.0f) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(LOCTEXT("FLandscapeEditorCustomNodeBuilder_ProceduralLayerWeight", "Weight")) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(0, 2) + .HAlign(HAlign_Left) + .FillWidth(1.0f) + [ + SNew(SNumericEntryBox) + .AllowSpin(true) + .MinValue(0.0f) + .MaxValue(65536.0f) + .MaxSliderValue(65536.0f) + .MinDesiredValueWidth(25.0f) + .Value(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GetLayerWeight, InLayerIndex) + .OnValueChanged(this, &FLandscapeEditorCustomNodeBuilder_ProceduralLayers::SetLayerWeight, InLayerIndex) + .IsEnabled(true) + ] + ]; + + return RowWidget; +} +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerTextCommitted(const FText& InText, ETextCommit::Type InCommitType, int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + LandscapeEdMode->SetProceduralLayerName(InLayerIndex, *InText.ToString()); + } +} + +FText FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GetLayerText(int32 InLayerIndex) const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + return FText::FromName(LandscapeEdMode->GetProceduralLayerName(InLayerIndex)); + } + + return FText::FromString(TEXT("None")); +} + +bool FLandscapeEditorCustomNodeBuilder_ProceduralLayers::IsLayerSelected(int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode) + { + return LandscapeEdMode->GetCurrentProceduralLayerIndex() == InLayerIndex; + } + + return false; +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerSelectionChanged(int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + if (LandscapeEdMode) + { + LandscapeEdMode->SetCurrentProceduralLayer(InLayerIndex); + LandscapeEdMode->UpdateTargetList(); + } +} + +TOptional FLandscapeEditorCustomNodeBuilder_ProceduralLayers::GetLayerWeight(int32 InLayerIndex) const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode) + { + return LandscapeEdMode->GetProceduralLayerWeight(InLayerIndex); + } + + return 1.0f; +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::SetLayerWeight(float InWeight, int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode) + { + LandscapeEdMode->SetProceduralLayerWeight(InWeight, InLayerIndex); + } +} + +void FLandscapeEditorCustomNodeBuilder_ProceduralLayers::OnLayerVisibilityChanged(ECheckBoxState NewState, int32 InLayerIndex) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode) + { + LandscapeEdMode->SetProceduralLayerVisibility(NewState == ECheckBoxState::Checked, InLayerIndex); + } +} + +ECheckBoxState FLandscapeEditorCustomNodeBuilder_ProceduralLayers::IsLayerVisible(int32 InLayerIndex) const +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode) + { + return LandscapeEdMode->IsProceduralLayerVisible(InLayerIndex) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + + return ECheckBoxState::Unchecked; +} + +FReply FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, int32 SlotIndex, SVerticalBox::FSlot* Slot) +{ + FEdModeLandscape* LandscapeEdMode = GetEditorMode(); + + if (LandscapeEdMode != nullptr) + { + // TODO: handle drag & drop + } + + return FReply::Unhandled(); +} + +TOptional FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleCanAcceptDrop(const FDragDropEvent& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, SVerticalBox::FSlot* Slot) +{ + // TODO: handle drag & drop + return TOptional(); +} + +FReply FLandscapeEditorCustomNodeBuilder_ProceduralLayers::HandleAcceptDrop(FDragDropEvent const& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, int32 SlotIndex, SVerticalBox::FSlot* Slot) +{ + // TODO: handle drag & drop + return FReply::Unhandled(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.h b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.h new file mode 100644 index 000000000000..924b9b5a5554 --- /dev/null +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_ProceduralLayers.h @@ -0,0 +1,77 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Misc/Attribute.h" +#include "Layout/Visibility.h" +#include "Layout/Margin.h" +#include "Styling/SlateColor.h" +#include "Input/Reply.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SWidget.h" +#include "Widgets/Layout/SBorder.h" +#include "Editor/LandscapeEditor/Private/LandscapeEdMode.h" +#include "IDetailCustomNodeBuilder.h" +#include "IDetailCustomization.h" +#include "AssetThumbnail.h" +#include "Framework/SlateDelegates.h" +#include "Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_Base.h" + +class FDetailWidgetRow; +class IDetailChildrenBuilder; +class IDetailLayoutBuilder; +class SDragAndDropVerticalBox; + +/** + * Slate widgets customizer for the layers list in the Landscape Editor + */ + +class FLandscapeEditorDetailCustomization_ProceduralLayers : public FLandscapeEditorDetailCustomization_Base +{ +public: + /** Makes a new instance of this detail layout class for a specific detail view requesting it */ + static TSharedRef MakeInstance(); + + /** IDetailCustomization interface */ + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; +}; + +class FLandscapeEditorCustomNodeBuilder_ProceduralLayers : public IDetailCustomNodeBuilder, public TSharedFromThis +{ +public: + FLandscapeEditorCustomNodeBuilder_ProceduralLayers(TSharedRef ThumbnailPool); + ~FLandscapeEditorCustomNodeBuilder_ProceduralLayers(); + + virtual void SetOnRebuildChildren( FSimpleDelegate InOnRegenerateChildren ) override; + virtual void GenerateHeaderRowContent( FDetailWidgetRow& NodeRow ) override; + virtual void GenerateChildContent( IDetailChildrenBuilder& ChildrenBuilder ) override; + virtual void Tick( float DeltaTime ) override {} + virtual bool RequiresTick() const override { return false; } + virtual bool InitiallyCollapsed() const override { return false; } + virtual FName GetName() const override { return "Layers"; } + +protected: + TSharedRef ThumbnailPool; + + static class FEdModeLandscape* GetEditorMode(); + + TSharedPtr GenerateRow(int32 InLayerIndex); + + // Drag/Drop handling + FReply HandleDragDetected(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent, int32 SlotIndex, SVerticalBox::FSlot* Slot); + TOptional HandleCanAcceptDrop(const FDragDropEvent& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, SVerticalBox::FSlot* Slot); + FReply HandleAcceptDrop(FDragDropEvent const& DragDropEvent, SDragAndDropVerticalBox::EItemDropZone DropZone, int32 SlotIndex, SVerticalBox::FSlot* Slot); + + bool IsLayerSelected(int32 LayerIndex); + void OnLayerSelectionChanged(int32 LayerIndex); + + void OnLayerTextCommitted(const FText& InText, ETextCommit::Type InCommitType, int32 InLayerIndex); + FText GetLayerText(int32 InLayerIndex) const; + + TOptional GetLayerWeight(int32 InLayerIndex) const; + void SetLayerWeight(float InWeight, int32 InLayerIndex); + + void OnLayerVisibilityChanged(ECheckBoxState NewState, int32 InLayerIndex); + ECheckBoxState IsLayerVisible(int32 InLayerIndex) const; +}; \ No newline at end of file diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_TargetLayers.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_TargetLayers.cpp index d5cd53565e09..c7eac0b498c4 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_TargetLayers.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_TargetLayers.cpp @@ -89,13 +89,15 @@ bool FLandscapeEditorDetailCustomization_TargetLayers::ShouldShowTargetLayers() FEdModeLandscape* LandscapeEdMode = GetEditorMode(); if (LandscapeEdMode && LandscapeEdMode->CurrentToolMode) { + const FName CurrentToolName = LandscapeEdMode->CurrentTool->GetToolName(); + //bool bSupportsHeightmap = (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes & ELandscapeToolTargetTypeMask::Heightmap) != 0; //bool bSupportsWeightmap = (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes & ELandscapeToolTargetTypeMask::Weightmap) != 0; //bool bSupportsVisibility = (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes & ELandscapeToolTargetTypeMask::Visibility) != 0; //// Visible if there are possible choices //if (bSupportsWeightmap || bSupportsHeightmap || bSupportsVisibility) - if (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes != 0) + if (LandscapeEdMode->CurrentToolMode->SupportedTargetTypes != 0 && CurrentToolName != TEXT("BPCustom")) { return true; } @@ -107,10 +109,13 @@ bool FLandscapeEditorDetailCustomization_TargetLayers::ShouldShowTargetLayers() bool FLandscapeEditorDetailCustomization_TargetLayers::ShouldShowPaintingRestriction() { FEdModeLandscape* LandscapeEdMode = GetEditorMode(); - if (LandscapeEdMode) + + if (LandscapeEdMode && LandscapeEdMode->CurrentToolMode) { - if (LandscapeEdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Weightmap || - LandscapeEdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Visibility) + const FName CurrentToolName = LandscapeEdMode->CurrentTool->GetToolName(); + + if ((LandscapeEdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Weightmap && CurrentToolName != TEXT("BPCustom")) + || LandscapeEdMode->CurrentToolTarget.TargetType == ELandscapeToolTargetType::Visibility) { return true; } @@ -270,6 +275,10 @@ void FLandscapeEditorCustomNodeBuilder_TargetLayers::GenerateHeaderRowContent(FD ] ] ]; + } + else + { + NodeRow.IsEnabled(false); } } diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.cpp b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.cpp index aa50a06a1237..025fc5f8842c 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.cpp @@ -19,6 +19,8 @@ #include "LandscapeEditorCommands.h" #include "LandscapeEditorDetailWidgets.h" +#include "LandscapeEditorDetailCustomization_ProceduralBrushStack.h" +#include "Settings/EditorExperimentalSettings.h" #define LOCTEXT_NAMESPACE "LandscapeEditor" @@ -109,6 +111,17 @@ void FLandscapeEditorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuild // Target Layers: Customization_TargetLayers = MakeShareable(new FLandscapeEditorDetailCustomization_TargetLayers); Customization_TargetLayers->CustomizeDetails(DetailBuilder); + + if (GetMutableDefault()->bProceduralLandscape) + { + // Brush Stack + Customization_ProceduralBrushStack = MakeShareable(new FLandscapeEditorDetailCustomization_ProceduralBrushStack); + Customization_ProceduralBrushStack->CustomizeDetails(DetailBuilder); + + // Procedural Layers + Customization_ProceduralLayers = MakeShareable(new FLandscapeEditorDetailCustomization_ProceduralLayers); + Customization_ProceduralLayers->CustomizeDetails(DetailBuilder); + } } FText FLandscapeEditorDetails::GetLocalizedName(FString Name) @@ -130,6 +143,11 @@ FText FLandscapeEditorDetails::GetLocalizedName(FString Name) LOCTEXT("ToolSet_Retopologize", "Retopologize"); LOCTEXT("ToolSet_Visibility", "Visibility"); + if (GetMutableDefault()->bProceduralLandscape) + { + LOCTEXT("ToolSet_BPCustom", "Blueprint Custom"); + } + LOCTEXT("ToolSet_Select", "Selection"); LOCTEXT("ToolSet_AddComponent", "Add"); LOCTEXT("ToolSet_DeleteComponent", "Delete"); @@ -305,6 +323,12 @@ TSharedRef FLandscapeEditorDetails::GetToolSelector() MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Noise"), NAME_None, LOCTEXT("Tool.Noise", "Noise"), LOCTEXT("Tool.Noise.Tooltip", "Adds noise to the heightmap or blend layer")); MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Retopologize"), NAME_None, LOCTEXT("Tool.Retopologize", "Retopologize"), LOCTEXT("Tool.Retopologize.Tooltip", "Automatically adjusts landscape vertices with an X/Y offset map to improve vertex density on cliffs, reducing texture stretching.\nNote: An X/Y offset map makes the landscape slower to render and paint on with other tools, so only use if needed")); MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Visibility"), NAME_None, LOCTEXT("Tool.Visibility", "Visibility"), LOCTEXT("Tool.Visibility.Tooltip", "Mask out individual quads in the landscape, leaving a hole.")); + + if (GetMutableDefault()->bProceduralLandscape) + { + MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_BPCustom"), NAME_None, LOCTEXT("Tool.SculptBPCustom", "Blueprint Custom"), LOCTEXT("Tool.SculptBPCustom.Tooltip", "Custom sculpting tools created using Blueprint.")); + } + MenuBuilder.EndSection(); MenuBuilder.BeginSection(NAME_None, LOCTEXT("RegionToolsTitle", "Region Tools")); @@ -321,6 +345,12 @@ TSharedRef FLandscapeEditorDetails::GetToolSelector() MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Smooth"), NAME_None, LOCTEXT("Tool.Smooth", "Smooth"), LOCTEXT("Tool.Smooth.Tooltip", "Smooths heightmaps or blend layers")); MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Flatten"), NAME_None, LOCTEXT("Tool.Flatten", "Flatten"), LOCTEXT("Tool.Flatten.Tooltip", "Flattens an area of heightmap or blend layer")); MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_Noise"), NAME_None, LOCTEXT("Tool.Noise", "Noise"), LOCTEXT("Tool.Noise.Tooltip", "Adds noise to the heightmap or blend layer")); + + if (GetMutableDefault()->bProceduralLandscape) + { + MenuBuilder.AddToolButton(NameToCommandMap.FindChecked("Tool_BPCustom"), NAME_None, LOCTEXT("Tool.PaintBPCustom", "Blueprint Custom"), LOCTEXT("Tool.PaintBPCustom.Tooltip", "Custom painting tools created using Blueprint.")); + } + MenuBuilder.EndSection(); } diff --git a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.h b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.h index 926c2544e2ee..dba6863aaf9e 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.h +++ b/Engine/Source/Editor/LandscapeEditor/Private/LandscapeEditorDetails.h @@ -9,6 +9,8 @@ #include "UnrealClient.h" #include "IDetailCustomization.h" #include "Editor/LandscapeEditor/Private/LandscapeEditorDetailCustomization_Base.h" +#include "LandscapeEditorDetailCustomization_ProceduralLayers.h" +#include "LandscapeEditorDetailCustomization_ProceduralBrushStack.h" class FLandscapeEditorDetailCustomization_AlphaBrush; class FLandscapeEditorDetailCustomization_CopyPaste; @@ -61,4 +63,6 @@ protected: TSharedPtr Customization_MiscTools; TSharedPtr Customization_AlphaBrush; TSharedPtr Customization_TargetLayers; + TSharedPtr Customization_ProceduralLayers; + TSharedPtr Customization_ProceduralBrushStack; }; diff --git a/Engine/Source/Editor/LandscapeEditor/Private/SLandscapeEditor.cpp b/Engine/Source/Editor/LandscapeEditor/Private/SLandscapeEditor.cpp index 57c8c8b860e0..c0963dafcf91 100644 --- a/Engine/Source/Editor/LandscapeEditor/Private/SLandscapeEditor.cpp +++ b/Engine/Source/Editor/LandscapeEditor/Private/SLandscapeEditor.cpp @@ -102,6 +102,7 @@ void FLandscapeToolKit::Init(const TSharedPtr& InitToolkitHost) MAP_TOOL("Noise"); MAP_TOOL("Retopologize"); MAP_TOOL("Visibility"); + MAP_TOOL("BPCustom"); MAP_TOOL("Select"); MAP_TOOL("AddComponent"); @@ -503,6 +504,15 @@ bool SLandscapeEditor::GetIsPropertyVisible(const FPropertyAndParent& PropertyAn return false; } } + if (Property.HasMetaData("ShowForBPCustomTool")) + { + const FName CurrentToolName = LandscapeEdMode->CurrentTool->GetToolName(); + + if (CurrentToolName != TEXT("BPCustom")) + { + return false; + } + } return true; } diff --git a/Engine/Source/Editor/LandscapeEditor/Public/LandscapeEditorObject.h b/Engine/Source/Editor/LandscapeEditor/Public/LandscapeEditorObject.h index 7c7989812173..6bb6306f4d08 100644 --- a/Engine/Source/Editor/LandscapeEditor/Public/LandscapeEditorObject.h +++ b/Engine/Source/Editor/LandscapeEditor/Public/LandscapeEditorObject.h @@ -8,6 +8,8 @@ #include "LandscapeProxy.h" #include "Editor/LandscapeEditor/Private/LandscapeEdMode.h" #include "LandscapeFileFormatInterface.h" +#include "LandscapeBPCustomBrush.h" + #include "LandscapeEditorObject.generated.h" class ULandscapeMaterialInstanceConstant; @@ -432,6 +434,11 @@ class ULandscapeEditorObject : public UObject UPROPERTY(Category="Tool Settings", EditAnywhere, NonTransactional, meta=(DisplayName="Smoothing Width", ShowForTools="Mirror", ClampMin="0", UIMin="0", UIMax="20")) int32 MirrorSmoothingWidth; + // BP Custom Tool + + UPROPERTY(Category = "Tool Settings", EditAnywhere, Transient, meta = (DisplayName = "Blueprint Brush", ShowForTools = "BPCustom")) + TSubclassOf BlueprintCustomBrush; + // Resize Landscape Tool // Number of quads per landscape component section diff --git a/Engine/Source/Editor/LandscapeEditor/Public/LandscapeToolInterface.h b/Engine/Source/Editor/LandscapeEditor/Public/LandscapeToolInterface.h index db11f41d80e3..fe3e82ee3694 100644 --- a/Engine/Source/Editor/LandscapeEditor/Public/LandscapeToolInterface.h +++ b/Engine/Source/Editor/LandscapeEditor/Public/LandscapeToolInterface.h @@ -176,12 +176,14 @@ struct FLandscapeToolTarget ELandscapeToolTargetType::Type TargetType; TWeakObjectPtr LayerInfo; FName LayerName; + int32 CurrentProceduralLayerIndex; FLandscapeToolTarget() : LandscapeInfo() , TargetType(ELandscapeToolTargetType::Heightmap) , LayerInfo() , LayerName(NAME_None) + , CurrentProceduralLayerIndex(INDEX_NONE) { } }; @@ -222,6 +224,9 @@ public: virtual bool IsSelectionAllowed(AActor* InActor, bool bInSelection) const { return false; } virtual bool UsesTransformWidget() const { return false; } virtual EAxisList::Type GetWidgetAxisToDraw(FWidget::EWidgetMode InWidgetMode) const { return EAxisList::All; } + + virtual bool OverrideWidgetLocation() const { return true; } + virtual bool OverrideWidgetRotation() const { return true; } virtual FVector GetWidgetLocation() const { return FVector::ZeroVector; } virtual FMatrix GetWidgetRotation() const { return FMatrix::Identity; } virtual bool DisallowMouseDeltaTracking() const { return false; } diff --git a/Engine/Source/Editor/MaterialEditor/Private/MaterialEditorInstanceDetailCustomization.cpp b/Engine/Source/Editor/MaterialEditor/Private/MaterialEditorInstanceDetailCustomization.cpp index a930de454a78..a3a26b4fcc02 100644 --- a/Engine/Source/Editor/MaterialEditor/Private/MaterialEditorInstanceDetailCustomization.cpp +++ b/Engine/Source/Editor/MaterialEditor/Private/MaterialEditorInstanceDetailCustomization.cpp @@ -241,6 +241,7 @@ void FMaterialInstanceParameterDetails::CustomizeDetails(IDetailLayoutBuilder& D PreviewingCategory.AddExternalObjectProperty(ExternalObjects, TEXT("PreviewMesh")); + DefaultCategory.AddExternalObjectProperty(ExternalObjects, TEXT("AssetUserData"), EPropertyLocation::Advanced); } void FMaterialInstanceParameterDetails::CreateGroupsWidget(TSharedRef ParameterGroupsProperty, IDetailCategoryBuilder& GroupsCategory) diff --git a/Engine/Source/Editor/MeshPaint/Private/IMeshPaintMode.cpp b/Engine/Source/Editor/MeshPaint/Private/IMeshPaintMode.cpp index 2280f35732ea..d18d52c295a6 100644 --- a/Engine/Source/Editor/MeshPaint/Private/IMeshPaintMode.cpp +++ b/Engine/Source/Editor/MeshPaint/Private/IMeshPaintMode.cpp @@ -182,13 +182,13 @@ void IMeshPaintEdMode::Exit() FEdMode::Exit(); } -bool IMeshPaintEdMode::CapturedMouseMove( FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY ) +bool IMeshPaintEdMode::ProcessCapturedMouseMoves( FEditorViewportClient* InViewportClient, FViewport* InViewport, const TArrayView& CapturedMouseMoves) { // We only care about perspective viewpo1rts bool bPaintApplied = false; if( InViewportClient->IsPerspective() ) { - if( MeshPainter->IsPainting() ) + if( MeshPainter->IsPainting() && CapturedMouseMoves.Num() > 0 ) { // Compute a world space ray from the screen space mouse coordinates FSceneViewFamilyContext ViewFamily( FSceneViewFamily::ConstructionValues( @@ -197,9 +197,18 @@ bool IMeshPaintEdMode::CapturedMouseMove( FEditorViewportClient* InViewportClien InViewportClient->EngineShowFlags) .SetRealtimeUpdate( InViewportClient->IsRealtime() )); FSceneView* View = InViewportClient->CalcSceneView( &ViewFamily ); - FViewportCursorLocation MouseViewportRay( View, (FEditorViewportClient*)InViewport->GetClient(), InMouseX, InMouseY ); + + TArray> Rays; + Rays.Reserve(CapturedMouseMoves.Num()); + + FEditorViewportClient* Client = (FEditorViewportClient*)InViewport->GetClient(); + for (int32 i = 0; i < CapturedMouseMoves.Num(); ++i) + { + FViewportCursorLocation MouseViewportRay(View, Client, CapturedMouseMoves[i].X, CapturedMouseMoves[i].Y); + Rays.Emplace(TPair(MouseViewportRay.GetOrigin(), MouseViewportRay.GetDirection())); + } - bPaintApplied = MeshPainter->Paint(InViewport, View->ViewMatrices.GetViewOrigin(), MouseViewportRay.GetOrigin(), MouseViewportRay.GetDirection()); + bPaintApplied = MeshPainter->Paint(InViewport, View->ViewMatrices.GetViewOrigin(), Rays); } } diff --git a/Engine/Source/Editor/MeshPaint/Private/IMeshPainter.cpp b/Engine/Source/Editor/MeshPaint/Private/IMeshPainter.cpp index 2fb0b96508d4..46746c101497 100644 --- a/Engine/Source/Editor/MeshPaint/Private/IMeshPainter.cpp +++ b/Engine/Source/Editor/MeshPaint/Private/IMeshPainter.cpp @@ -98,7 +98,18 @@ bool IMeshPainter::Paint(FViewport* Viewport, const FVector& InCameraOrigin, con const float PaintStrength = Viewport->IsPenActive() ? Viewport->GetTabletPressure() : 1.f; // Handle internal painting functionality - return PaintInternal(InCameraOrigin, InRayOrigin, InRayDirection, PaintAction, PaintStrength); + TPair Ray(InRayOrigin, InRayDirection); + return PaintInternal(InCameraOrigin, MakeArrayView(&Ray, 1), PaintAction, PaintStrength); +} + +bool IMeshPainter::Paint(FViewport* Viewport, const FVector& InCameraOrigin, const TArrayView>& Rays) +{ + // Determine paint action according to whether or not shift is held down + const EMeshPaintAction PaintAction = (Viewport->KeyState(EKeys::LeftShift) || Viewport->KeyState(EKeys::RightShift)) ? EMeshPaintAction::Erase : EMeshPaintAction::Paint; + + const float PaintStrength = Viewport->IsPenActive() ? Viewport->GetTabletPressure() : 1.f; + // Handle internal painting functionality + return PaintInternal(InCameraOrigin, Rays, PaintAction, PaintStrength); } bool IMeshPainter::PaintVR(FViewport* Viewport, const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, UVREditorInteractor* VRInteractor) @@ -115,7 +126,8 @@ bool IMeshPainter::PaintVR(FViewport* Viewport, const FVector& InCameraOrigin, c PaintAction = bIsModifierPressed ? EMeshPaintAction::Erase : EMeshPaintAction::Paint; // Handle internal painting functionality - bPaintApplied = PaintInternal(InCameraOrigin, InRayOrigin, InRayDirection, PaintAction, StrengthScale); + TPair Ray(InRayOrigin, InRayDirection); + bPaintApplied = PaintInternal(InCameraOrigin, MakeArrayView(&Ray, 1), PaintAction, StrengthScale); if (bPaintApplied) { diff --git a/Engine/Source/Editor/MeshPaint/Private/MeshPaintHelpers.cpp b/Engine/Source/Editor/MeshPaint/Private/MeshPaintHelpers.cpp index 2ce862286191..732de3a203a8 100644 --- a/Engine/Source/Editor/MeshPaint/Private/MeshPaintHelpers.cpp +++ b/Engine/Source/Editor/MeshPaint/Private/MeshPaintHelpers.cpp @@ -1573,7 +1573,7 @@ FColor MeshPaintHelpers::PickVertexColorFromTextureData(const uint8* MipData, co return VertexColor; } -bool MeshPaintHelpers::ApplyPerVertexPaintAction(FPerVertexPaintActionArgs& InArgs, FPerVertexPaintAction Action) +bool MeshPaintHelpers::GetPerVertexPaintInfluencedVertices(FPerVertexPaintActionArgs& InArgs, TSet& InfluencedVertices) { // Retrieve components world matrix const FMatrix& ComponentToWorldMatrix = InArgs.Adapter->GetComponentToWorldMatrix(); @@ -1588,24 +1588,17 @@ bool MeshPaintHelpers::ApplyPerVertexPaintAction(FPerVertexPaintActionArgs& InAr const float ComponentSpaceSquaredBrushRadius = ComponentSpaceBrushRadius * ComponentSpaceBrushRadius; // Get a list of unique vertices indexed by the influenced triangles - TSet InfluencedVertices; InArgs.Adapter->GetInfluencedVertexIndices(ComponentSpaceSquaredBrushRadius, ComponentSpaceBrushPosition, ComponentSpaceCameraPosition, InArgs.BrushSettings->bOnlyFrontFacingTriangles, InfluencedVertices); + return (InfluencedVertices.Num() > 0); +} + +bool MeshPaintHelpers::ApplyPerVertexPaintAction(FPerVertexPaintActionArgs& InArgs, FPerVertexPaintAction Action) +{ + // Get a list of unique vertices indexed by the influenced triangles + TSet InfluencedVertices; + GetPerVertexPaintInfluencedVertices(InArgs, InfluencedVertices); - const int32 NumParallelFors = 4; - const int32 NumPerFor = FMath::CeilToInt((float)InfluencedVertices.Num() / NumParallelFors); - - // Parallel applying - /*ParallelFor(NumParallelFors, [=](int32 Index) - { - const int32 Start = Index * NumPerFor; - const int32 End = FMath::Min(Start + NumPerFor, InfluencedVertices.Num()); - - for (int32 VertexIndex = Start; VertexIndex < End; ++VertexIndex) - { - Action.ExecuteIfBound(Adapter, VertexIndex); - } - });*/ if (InfluencedVertices.Num()) { InArgs.Adapter->PreEdit(); diff --git a/Engine/Source/Editor/MeshPaint/Public/IMeshPaintMode.h b/Engine/Source/Editor/MeshPaint/Public/IMeshPaintMode.h index ad76f6220f65..f5cc90dff38c 100644 --- a/Engine/Source/Editor/MeshPaint/Public/IMeshPaintMode.h +++ b/Engine/Source/Editor/MeshPaint/Public/IMeshPaintMode.h @@ -36,7 +36,7 @@ public: virtual bool UsesToolkits() const override { return true; } virtual void Enter() override; virtual void Exit() override; - virtual bool CapturedMouseMove( FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY ) override; + virtual bool ProcessCapturedMouseMoves(FEditorViewportClient* InViewportClient, FViewport* InViewport, const TArrayView& CapturedMouseMoves) override; virtual bool StartTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override { return true; } virtual bool EndTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override; virtual bool InputKey( FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent ) override; @@ -83,6 +83,7 @@ private: /** Will store the state of selection locks on start of paint mode so that it can be restored on close */ bool bWasSelectionLockedOnStart; + /** Delegate handle for registered selection change lambda */ FDelegateHandle SelectionChangedHandle; protected: diff --git a/Engine/Source/Editor/MeshPaint/Public/IMeshPainter.h b/Engine/Source/Editor/MeshPaint/Public/IMeshPainter.h index 1e0ec697e1de..5f00ff8426df 100644 --- a/Engine/Source/Editor/MeshPaint/Public/IMeshPainter.h +++ b/Engine/Source/Editor/MeshPaint/Public/IMeshPainter.h @@ -48,6 +48,9 @@ public: /** 'Normal' painting functionality, called when the user tries to paint on a mesh using the mouse */ virtual bool Paint(FViewport* Viewport, const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection); + /** 'Normal' painting functionality, called when the user tries to paint on a mesh using the mouse */ + virtual bool Paint(FViewport* Viewport, const FVector& InCameraOrigin, const TArrayView>& Rays); + /** VR painting functionality, called when the user tries to paint on a mesh using a VR controller */ virtual bool PaintVR(FViewport* Viewport, const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, UVREditorInteractor* VRInteractor); @@ -87,7 +90,7 @@ public: virtual void Reset() = 0; protected: /** Internal painting functionality, is called from Paint and PaintVR and implemented in the derived painters */ - virtual bool PaintInternal(const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, EMeshPaintAction PaintAction, float PaintStrength) = 0; + virtual bool PaintInternal(const FVector& InCameraOrigin, const TArrayView>& Rays, EMeshPaintAction PaintAction, float PaintStrength) = 0; /** Renders viewport interactor widget */ void RenderInteractors(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI, bool bRenderVertices, ESceneDepthPriorityGroup DepthGroup = SDPG_World); diff --git a/Engine/Source/Editor/MeshPaint/Public/MeshPaintHelpers.h b/Engine/Source/Editor/MeshPaint/Public/MeshPaintHelpers.h index d008cf09c070..063896958380 100644 --- a/Engine/Source/Editor/MeshPaint/Public/MeshPaintHelpers.h +++ b/Engine/Source/Editor/MeshPaint/Public/MeshPaintHelpers.h @@ -119,7 +119,9 @@ public: /** Given arguments for an action, and an action - retrieves influences vertices and applies Action to them */ static bool ApplyPerVertexPaintAction(FPerVertexPaintActionArgs& InArgs, FPerVertexPaintAction Action); - + + static bool GetPerVertexPaintInfluencedVertices(FPerVertexPaintActionArgs& InArgs, TSet& InfluencedVertices); + /** Given the adapter, settings and view-information retrieves influences triangles and applies Action to them */ static bool ApplyPerTrianglePaintAction(IMeshPaintGeometryAdapter* Adapter, const FVector& CameraPosition, const FVector& HitPosition, const UPaintBrushSettings* Settings, FPerTrianglePaintAction Action); diff --git a/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.cpp b/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.cpp index 2f9c3e736652..541e70e263b8 100644 --- a/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.cpp +++ b/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.cpp @@ -785,17 +785,35 @@ void FPaintModePainter::FinishPainting() } } -bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, EMeshPaintAction PaintAction, float PaintStrength) +bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const TArrayView>& Rays, EMeshPaintAction PaintAction, float PaintStrength) { + struct FPaintRayResults + { + FMeshPaintParameters Params; + FHitResult BestTraceResult; + }; + TArray PaintRayResults; + PaintRayResults.AddDefaulted(Rays.Num()); + + TMap> HoveredComponents; + const float BrushRadius = BrushSettings->GetBrushRadius(); + const bool bIsPainting = (PaintAction == EMeshPaintAction::Paint); + const float InStrengthScale = PaintStrength;; + + bool bPaintApplied = false; + // Fire out a ray to see if there is a *selected* component under the mouse cursor that can be painted. // NOTE: We can't use a GWorld line check for this as that would ignore components that have collision disabled - TArray HoveredComponents; - FHitResult BestTraceResult; + for (int32 i=0; i < Rays.Num(); ++i) { - const FVector TraceStart(InRayOrigin); - const FVector TraceEnd(InRayOrigin + InRayDirection * HALF_WORLD_MAX); + const FVector& RayOrigin = Rays[i].Key; + const FVector& RayDirection = Rays[i].Value; + FHitResult& BestTraceResult = PaintRayResults[i].BestTraceResult; + + const FVector TraceStart(RayOrigin); + const FVector TraceEnd(RayOrigin + RayDirection * HALF_WORLD_MAX); for (UMeshComponent* MeshComponent : PaintableComponents) { @@ -819,6 +837,8 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect } } + bool bUsed = false; + if (BestTraceResult.GetComponent() != nullptr) { // If we're using texture paint, just use the best trace result we found as we currently only @@ -826,7 +846,8 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect if (PaintSettings->PaintMode == EPaintMode::Textures) { UMeshComponent* ComponentToPaint = CastChecked(BestTraceResult.GetComponent()); - HoveredComponents.AddUnique(ComponentToPaint); + HoveredComponents.FindOrAdd(ComponentToPaint).Add(i); + bUsed = true; } else { @@ -840,18 +861,69 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect if (ComponentToAdapterMap.Contains(TestComponent) && ComponentBounds.Intersect(BrushBounds)) { // OK, this mesh potentially overlaps the brush! - HoveredComponents.AddUnique(TestComponent); + HoveredComponents.FindOrAdd(TestComponent).Add(i); + bUsed = true; } } } } + + if (bUsed) + { + FVector BrushXAxis, BrushYAxis; + BestTraceResult.Normal.FindBestAxisVectors(BrushXAxis, BrushYAxis); + // Display settings + const float VisualBiasDistance = 0.15f; + const FVector BrushVisualPosition = BestTraceResult.Location + BestTraceResult.Normal * VisualBiasDistance; + + const FLinearColor PaintColor = (PaintSettings->PaintMode == EPaintMode::Vertices) ? (PaintSettings->VertexPaintSettings.PaintColor) : PaintSettings->TexturePaintSettings.PaintColor; + const FLinearColor EraseColor = (PaintSettings->PaintMode == EPaintMode::Vertices) ? (PaintSettings->VertexPaintSettings.EraseColor) : PaintSettings->TexturePaintSettings.EraseColor; + + // NOTE: We square the brush strength to maximize slider precision in the low range + const float BrushStrength = + BrushSettings->BrushStrength * BrushSettings->BrushStrength * + InStrengthScale; + + const float BrushDepth = BrushRadius; + + // Mesh paint settings + FMeshPaintParameters& Params = PaintRayResults[i].Params; + { + Params.PaintMode = PaintSettings->VertexPaintSettings.MeshPaintMode; + Params.PaintAction = PaintAction; + Params.BrushPosition = BestTraceResult.Location; + Params.BrushNormal = BestTraceResult.Normal; + Params.BrushColor = bIsPainting ? PaintColor : EraseColor; + Params.SquaredBrushRadius = BrushRadius * BrushRadius; + Params.BrushRadialFalloffRange = BrushSettings->BrushFalloffAmount * BrushRadius; + Params.InnerBrushRadius = BrushRadius - Params.BrushRadialFalloffRange; + Params.BrushDepth = BrushDepth; + Params.BrushDepthFalloffRange = BrushSettings->BrushFalloffAmount * BrushDepth; + Params.InnerBrushDepth = BrushDepth - Params.BrushDepthFalloffRange; + Params.BrushStrength = BrushStrength; + Params.BrushToWorldMatrix = FMatrix(BrushXAxis, BrushYAxis, Params.BrushNormal, Params.BrushPosition); + Params.InverseBrushToWorldMatrix = Params.BrushToWorldMatrix.InverseFast(); + Params.bWriteRed = PaintSettings->VertexPaintSettings.bWriteRed; + Params.bWriteGreen = PaintSettings->VertexPaintSettings.bWriteGreen; + Params.bWriteBlue = PaintSettings->VertexPaintSettings.bWriteBlue; + Params.bWriteAlpha = PaintSettings->VertexPaintSettings.bWriteAlpha; + Params.TotalWeightCount = (int32)PaintSettings->VertexPaintSettings.TextureWeightType; + + // Select texture weight index based on whether or not we're painting or erasing + { + const int32 PaintWeightIndex = bIsPainting ? (int32)PaintSettings->VertexPaintSettings.PaintTextureWeightIndex : (int32)PaintSettings->VertexPaintSettings.EraseTextureWeightIndex; + + // Clamp the weight index to fall within the total weight count + Params.PaintWeightIndex = FMath::Clamp(PaintWeightIndex, 0, Params.TotalWeightCount - 1); + } + + // @todo MeshPaint: Ideally we would default to: TexturePaintingCurrentMeshComponent->StaticMesh->LightMapCoordinateIndex + // Or we could indicate in the GUI which channel is the light map set (button to set it?) + Params.UVChannel = PaintSettings->TexturePaintSettings.UVChannel; + } + } } - const bool bIsPainting = (PaintAction == EMeshPaintAction::Paint); - const float InStrengthScale = PaintStrength;; - - bool bPaintApplied = false; - if (HoveredComponents.Num() > 0) { if (bArePainting == false) @@ -864,62 +936,13 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect bArePainting = true; TimeSinceStartedPainting = 0.0f; } - - FVector BrushXAxis, BrushYAxis; - BestTraceResult.Normal.FindBestAxisVectors(BrushXAxis, BrushYAxis); - // Display settings - const float VisualBiasDistance = 0.15f; - const FVector BrushVisualPosition = BestTraceResult.Location + BestTraceResult.Normal * VisualBiasDistance; - - const FLinearColor PaintColor = (PaintSettings->PaintMode == EPaintMode::Vertices) ? (PaintSettings->VertexPaintSettings.PaintColor) : PaintSettings->TexturePaintSettings.PaintColor; - const FLinearColor EraseColor = (PaintSettings->PaintMode == EPaintMode::Vertices) ? (PaintSettings->VertexPaintSettings.EraseColor) : PaintSettings->TexturePaintSettings.EraseColor; - - // NOTE: We square the brush strength to maximize slider precision in the low range - const float BrushStrength = - BrushSettings->BrushStrength * BrushSettings->BrushStrength * - InStrengthScale; - - const float BrushDepth = BrushRadius; - - // Mesh paint settings - FMeshPaintParameters Params; - { - Params.PaintMode = PaintSettings->VertexPaintSettings.MeshPaintMode; - Params.PaintAction = PaintAction; - Params.BrushPosition = BestTraceResult.Location; - Params.BrushNormal = BestTraceResult.Normal; - Params.BrushColor = bIsPainting ? PaintColor : EraseColor; - Params.SquaredBrushRadius = BrushRadius * BrushRadius; - Params.BrushRadialFalloffRange = BrushSettings->BrushFalloffAmount * BrushRadius; - Params.InnerBrushRadius = BrushRadius - Params.BrushRadialFalloffRange; - Params.BrushDepth = BrushDepth; - Params.BrushDepthFalloffRange = BrushSettings->BrushFalloffAmount * BrushDepth; - Params.InnerBrushDepth = BrushDepth - Params.BrushDepthFalloffRange; - Params.BrushStrength = BrushStrength; - Params.BrushToWorldMatrix = FMatrix(BrushXAxis, BrushYAxis, Params.BrushNormal, Params.BrushPosition); - Params.InverseBrushToWorldMatrix = Params.BrushToWorldMatrix.InverseFast(); - Params.bWriteRed = PaintSettings->VertexPaintSettings.bWriteRed; - Params.bWriteGreen = PaintSettings->VertexPaintSettings.bWriteGreen; - Params.bWriteBlue = PaintSettings->VertexPaintSettings.bWriteBlue; - Params.bWriteAlpha = PaintSettings->VertexPaintSettings.bWriteAlpha; - Params.TotalWeightCount = (int32)PaintSettings->VertexPaintSettings.TextureWeightType; - - // Select texture weight index based on whether or not we're painting or erasing - { - const int32 PaintWeightIndex = bIsPainting ? (int32)PaintSettings->VertexPaintSettings.PaintTextureWeightIndex : (int32)PaintSettings->VertexPaintSettings.EraseTextureWeightIndex; - - // Clamp the weight index to fall within the total weight count - Params.PaintWeightIndex = FMath::Clamp(PaintWeightIndex, 0, Params.TotalWeightCount - 1); - } - - // @todo MeshPaint: Ideally we would default to: TexturePaintingCurrentMeshComponent->StaticMesh->LightMapCoordinateIndex - // Or we could indicate in the GUI which channel is the light map set (button to set it?) - Params.UVChannel = PaintSettings->TexturePaintSettings.UVChannel; - } - + // Iterate over the selected meshes under the cursor and paint them! - for (UMeshComponent* HoveredComponent : HoveredComponents) + for (auto& Entry : HoveredComponents) { + UMeshComponent* HoveredComponent = Entry.Key; + TArray& PaintRayResultIds = Entry.Value; + IMeshPaintGeometryAdapter* MeshAdapter = ComponentToAdapterMap.FindRef(HoveredComponent).Get(); if (!ensure(MeshAdapter)) { @@ -928,15 +951,42 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect if (PaintSettings->PaintMode == EPaintMode::Vertices && MeshAdapter->SupportsVertexPaint()) { - FPerVertexPaintActionArgs Args; Args.Adapter = MeshAdapter; Args.CameraPosition = InCameraOrigin; - Args.HitResult = BestTraceResult; Args.BrushSettings = BrushSettings; Args.Action = PaintAction; - bPaintApplied |= MeshPaintHelpers::ApplyPerVertexPaintAction(Args, FPerVertexPaintAction::CreateRaw(this, &FPaintModePainter::ApplyVertexColor, Params)); + bool bMeshPreEditCalled = false; + + TSet InfluencedVertices; + for (int32 PaintRayResultId : PaintRayResultIds) + { + InfluencedVertices.Reset(); + Args.HitResult = PaintRayResults[PaintRayResultId].BestTraceResult; + bPaintApplied |= MeshPaintHelpers::GetPerVertexPaintInfluencedVertices(Args, InfluencedVertices); + + if (InfluencedVertices.Num() == 0) + { + continue; + } + + if (!bMeshPreEditCalled) + { + bMeshPreEditCalled = true; + MeshAdapter->PreEdit(); + } + + for (const int32 VertexIndex : InfluencedVertices) + { + FPaintModePainter::ApplyVertexColor(Args, VertexIndex, PaintRayResults[PaintRayResultId].Params); + } + } + + if (bMeshPreEditCalled) + { + MeshAdapter->PostEdit(); + } } else if (PaintSettings->PaintMode == EPaintMode::Textures&& MeshAdapter->SupportsTexturePaint()) { @@ -956,7 +1006,12 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect TexturePaintHelpers::RetrieveMeshSectionsForTextures(HoveredComponent, CachedLODIndex, Textures, MaterialSections); TArray TrianglePaintInfoArray; - bPaintApplied |= MeshPaintHelpers::ApplyPerTrianglePaintAction(MeshAdapter, InCameraOrigin, BestTraceResult.Location, BrushSettings, FPerTrianglePaintAction::CreateRaw(this, &FPaintModePainter::GatherTextureTriangles, &TrianglePaintInfoArray, &MaterialSections, PaintSettings->TexturePaintSettings.UVChannel)); + for (int32 PaintRayResultId : PaintRayResultIds) + { + const FVector& BestTraceResultLocation = PaintRayResults[PaintRayResultId].BestTraceResult.Location; + bPaintApplied |= MeshPaintHelpers::ApplyPerTrianglePaintAction(MeshAdapter, InCameraOrigin, BestTraceResultLocation, BrushSettings, FPerTrianglePaintAction::CreateRaw(this, &FPaintModePainter::GatherTextureTriangles, &TrianglePaintInfoArray, &MaterialSections, PaintSettings->TexturePaintSettings.UVChannel)); + break; + } // Painting textures if ((TexturePaintingCurrentMeshComponent != nullptr) && (TexturePaintingCurrentMeshComponent != HoveredComponent)) @@ -972,12 +1027,17 @@ bool FPaintModePainter::PaintInternal(const FVector& InCameraOrigin, const FVect if (TexturePaintingCurrentMeshComponent != nullptr) { - Params.bWriteRed = PaintSettings->TexturePaintSettings.bWriteRed; - Params.bWriteGreen = PaintSettings->TexturePaintSettings.bWriteGreen; - Params.bWriteBlue = PaintSettings->TexturePaintSettings.bWriteBlue; - Params.bWriteAlpha = PaintSettings->TexturePaintSettings.bWriteAlpha; + for (int32 PaintRayResultId : PaintRayResultIds) + { + FMeshPaintParameters& Params = PaintRayResults[PaintRayResultId].Params; + Params.bWriteRed = PaintSettings->TexturePaintSettings.bWriteRed; + Params.bWriteGreen = PaintSettings->TexturePaintSettings.bWriteGreen; + Params.bWriteBlue = PaintSettings->TexturePaintSettings.bWriteBlue; + Params.bWriteAlpha = PaintSettings->TexturePaintSettings.bWriteAlpha; - PaintTexture(Params, TrianglePaintInfoArray, *MeshAdapter); + PaintTexture(Params, TrianglePaintInfoArray, *MeshAdapter); + break; + } } } } diff --git a/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.h b/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.h index 6e9057010dc3..c695e44c650a 100644 --- a/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.h +++ b/Engine/Source/Editor/MeshPaintMode/Private/PaintModePainter.h @@ -103,7 +103,7 @@ public: protected: /** Override from IMeshPainter used for applying actual painting */ - virtual bool PaintInternal(const FVector& InCameraOrigin, const FVector& InRayOrigin, const FVector& InRayDirection, EMeshPaintAction PaintAction, float PaintStrength) override; + virtual bool PaintInternal(const FVector& InCameraOrigin, const TArrayView>& Rays, EMeshPaintAction PaintAction, float PaintStrength) override; /** Per vertex action function used for painting vertex colors */ void ApplyVertexColor(FPerVertexPaintActionArgs& Args, int32 VertexIndex, FMeshPaintParameters Parameters); diff --git a/Engine/Source/Editor/MovieSceneTools/Private/MovieSceneToolHelpers.cpp b/Engine/Source/Editor/MovieSceneTools/Private/MovieSceneToolHelpers.cpp index 2ee95935e86f..3986e88e5f82 100644 --- a/Engine/Source/Editor/MovieSceneTools/Private/MovieSceneToolHelpers.cpp +++ b/Engine/Source/Editor/MovieSceneTools/Private/MovieSceneToolHelpers.cpp @@ -923,9 +923,9 @@ bool ImportFBXProperty(FString NodeName, FString AnimatedPropertyName, FGuid Obj for (auto SourceIt = Source.GetKeyHandleIterator(); SourceIt; ++SourceIt) { - FRichCurveKey &Key = Source.GetKey(SourceIt.Key()); + FRichCurveKey &Key = Source.GetKey(*SourceIt); float ArriveTangent = Key.ArriveTangent; - FKeyHandle PrevKeyHandle = Source.GetPreviousKey(SourceIt.Key()); + FKeyHandle PrevKeyHandle = Source.GetPreviousKey(*SourceIt); if (Source.IsKeyHandleValid(PrevKeyHandle)) { FRichCurveKey &PrevKey = Source.GetKey(PrevKeyHandle); @@ -933,7 +933,7 @@ bool ImportFBXProperty(FString NodeName, FString AnimatedPropertyName, FGuid Obj } float LeaveTangent = Key.LeaveTangent; - FKeyHandle NextKeyHandle = Source.GetNextKey(SourceIt.Key()); + FKeyHandle NextKeyHandle = Source.GetNextKey(*SourceIt); if (Source.IsKeyHandleValid(NextKeyHandle)) { FRichCurveKey &NextKey = Source.GetKey(NextKeyHandle); @@ -976,9 +976,9 @@ void ImportTransformChannel(const FRichCurve& Source, FMovieSceneFloatChannel* D ChannelData.Reset(); for (auto SourceIt = Source.GetKeyHandleIterator(); SourceIt; ++SourceIt) { - const FRichCurveKey Key = Source.GetKey(SourceIt.Key()); + const FRichCurveKey Key = Source.GetKey(*SourceIt); float ArriveTangent = Key.ArriveTangent; - FKeyHandle PrevKeyHandle = Source.GetPreviousKey(SourceIt.Key()); + FKeyHandle PrevKeyHandle = Source.GetPreviousKey(*SourceIt); if (Source.IsKeyHandleValid(PrevKeyHandle)) { const FRichCurveKey PrevKey = Source.GetKey(PrevKeyHandle); @@ -986,7 +986,7 @@ void ImportTransformChannel(const FRichCurve& Source, FMovieSceneFloatChannel* D } float LeaveTangent = Key.LeaveTangent; - FKeyHandle NextKeyHandle = Source.GetNextKey(SourceIt.Key()); + FKeyHandle NextKeyHandle = Source.GetNextKey(*SourceIt); if (Source.IsKeyHandleValid(NextKeyHandle)) { const FRichCurveKey NextKey = Source.GetKey(NextKeyHandle); diff --git a/Engine/Source/Editor/Persona/Private/PersonaMeshDetails.cpp b/Engine/Source/Editor/Persona/Private/PersonaMeshDetails.cpp index a438517a161d..7b99d80b91e9 100644 --- a/Engine/Source/Editor/Persona/Private/PersonaMeshDetails.cpp +++ b/Engine/Source/Editor/Persona/Private/PersonaMeshDetails.cpp @@ -1432,7 +1432,7 @@ void FPersonaMeshDetails::RegenerateOneLOD(int32 LODIndex) FSkeletalMeshLODInfo& CurrentLODInfo = *(SkelMesh->GetLODInfo(LODIndex)); if (LODIndex == CurrentLODInfo.ReductionSettings.BaseLOD && CurrentLODInfo.bHasBeenSimplified - && !CurrentLODInfo.ReductionSettings.IsReductionSettingActive() + && !SkelMesh->IsReductionActive(LODIndex) && SkelMesh->GetImportedModel()->OriginalReductionSourceMeshData.IsValidIndex(LODIndex) && !SkelMesh->GetImportedModel()->OriginalReductionSourceMeshData[LODIndex]->IsEmpty()) { @@ -1487,7 +1487,8 @@ FReply FPersonaMeshDetails::RegenerateLOD(int32 LODIndex) if (SkelMesh->IsValidLODIndex(LODIndex)) { FSkeletalMeshLODInfo& CurrentLODInfo = *(SkelMesh->GetLODInfo(LODIndex)); - if (CurrentLODInfo.bHasBeenSimplified == false && (LODIndex > 0 || CurrentLODInfo.ReductionSettings.IsReductionSettingActive())) + bool bIsReductionActive = SkelMesh->IsReductionActive(LODIndex); + if (CurrentLODInfo.bHasBeenSimplified == false && (LODIndex > 0 || bIsReductionActive)) { if (LODIndex > 0) { @@ -1498,7 +1499,7 @@ FReply FPersonaMeshDetails::RegenerateLOD(int32 LODIndex) return FReply::Handled(); } } - else if (CurrentLODInfo.ReductionSettings.IsReductionSettingActive()) + else if (bIsReductionActive) { //Ask user a special permission when the base LOD can be reduce const FText Text(LOCTEXT("Warning_ReductionApplyingToImportedMesh_ReduceBaseLOD", "Are you sure you'd like to apply mesh reduction to the base LOD?")); @@ -1610,13 +1611,14 @@ void FPersonaMeshDetails::ApplyChanges() for (int32 LODIdx = 0; LODIdx < LODCount; LODIdx++) { FSkeletalMeshLODInfo& CurrentLODInfo = *(SkelMesh->GetLODInfo(LODIdx)); - if (CurrentLODInfo.bHasBeenSimplified == false && (LODIdx > 0 || CurrentLODInfo.ReductionSettings.IsReductionSettingActive())) + bool bIsReductionActive = SkelMesh->IsReductionActive(LODIdx); + if (CurrentLODInfo.bHasBeenSimplified == false && (LODIdx > 0 || bIsReductionActive)) { if (LODIdx > 0) { bImportedLODs = true; } - else if (CurrentLODInfo.ReductionSettings.IsReductionSettingActive()) + else if (bIsReductionActive) { //Ask user a special permission when the base LOD can be reduce const FText Text(LOCTEXT("Warning_ReductionApplyingToImportedMesh_ReduceBaseLOD", "Are you sure you'd like to apply mesh reduction to the base LOD?")); @@ -1630,7 +1632,7 @@ void FPersonaMeshDetails::ApplyChanges() } else if(LODIdx == CurrentLODInfo.ReductionSettings.BaseLOD && CurrentLODInfo.bHasBeenSimplified - && !CurrentLODInfo.ReductionSettings.IsReductionSettingActive() + && !bIsReductionActive && SkelMesh->GetImportedModel()->OriginalReductionSourceMeshData.IsValidIndex(0) && !SkelMesh->GetImportedModel()->OriginalReductionSourceMeshData[0]->IsEmpty()) { diff --git a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditor.cpp b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditor.cpp index d6d5107f6bab..ffb5c5a615f7 100644 --- a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditor.cpp +++ b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditor.cpp @@ -1380,7 +1380,8 @@ void FPhysicsAssetEditor::AddNewPrimitive(EAggCollisionShape::Type InPrimitiveTy int32 BoneIndex = SharedData->EditorSkelComp->GetBoneIndex(BoneProxy->BoneName); if (BoneIndex != INDEX_NONE) { - int32 NewBodyIndex = FPhysicsAssetUtils::CreateNewBody(SharedData->PhysicsAsset, BoneProxy->BoneName); + const FPhysAssetCreateParams& NewBodyData = GetDefault()->CreateParams; + int32 NewBodyIndex = FPhysicsAssetUtils::CreateNewBody(SharedData->PhysicsAsset, BoneProxy->BoneName, NewBodyData); NewSelection.AddUnique(FPhysicsAssetEditorSharedData::FSelection(NewBodyIndex, EAggCollisionShape::Unknown, 0)); } } @@ -2767,6 +2768,7 @@ void FPhysicsAssetEditor::HandlePreviewSceneCreated(const TSharedRefEditorSkelComp = NewObject(Actor); + SharedData->EditorSkelComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); SharedData->EditorSkelComp->SharedData = SharedData.Get(); SharedData->EditorSkelComp->SetSkeletalMesh(SharedData->PhysicsAsset->GetPreviewMesh()); SharedData->EditorSkelComp->SetPhysicsAsset(SharedData->PhysicsAsset, true); diff --git a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSharedData.cpp b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSharedData.cpp index 5cd75b453bd1..bb3b5994a8d7 100644 --- a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSharedData.cpp +++ b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSharedData.cpp @@ -92,7 +92,7 @@ void FPhysicsAssetEditorSharedData::Initialize(const TSharedRefSkeletalBodySetups.Num(); ++i) { UBodySetup* BodySetup = PhysicsAsset->SkeletalBodySetups[i]; - if (BodySetup->AggGeom.GetElementCount() == 0) + if (BodySetup && BodySetup->AggGeom.GetElementCount() == 0) { FKBoxElem BoxElem; BoxElem.SetTransform(FTransform::Identity); @@ -129,6 +129,10 @@ void FPhysicsAssetEditorSharedData::Initialize(const TSharedRefSkeletalBodySetups.Num(); ++i) { + if (!ensure(PhysicsAsset->SkeletalBodySetups[i])) + { + continue; + } FName BoneName = PhysicsAsset->SkeletalBodySetups[i]->BoneName; int32 BoneIndex = EditorSkelMesh->RefSkeleton.FindBoneIndex(BoneName); if (BoneIndex == INDEX_NONE) @@ -557,6 +561,10 @@ void FPhysicsAssetEditorSharedData::UpdateNoCollisionBodies() // Query disable table with selected body and every other body. for (int32 i = 0; i SkeletalBodySetups.Num(); ++i) { + if (!ensure(PhysicsAsset->SkeletalBodySetups[i])) + { + continue; + } // Add any bodies with bNoCollision if (PhysicsAsset->SkeletalBodySetups[i]->DefaultInstance.GetCollisionEnabled() == ECollisionEnabled::NoCollision) { @@ -564,6 +572,10 @@ void FPhysicsAssetEditorSharedData::UpdateNoCollisionBodies() } else if (GetSelectedBody() && i != GetSelectedBody()->Index) { + if (!ensure(PhysicsAsset->SkeletalBodySetups[GetSelectedBody()->Index])) + { + continue; + } // Add this body if it has disabled collision with selected. FRigidBodyIndexPair Key(i, GetSelectedBody()->Index); @@ -990,8 +1002,10 @@ void FPhysicsAssetEditorSharedData::MakeNewBody(int32 NewBoneIndex, bool bAutoSe // Find body that currently controls this bone. int32 ParentBodyIndex = PhysicsAsset->FindControllingBodyIndex(EditorSkelMesh, NewBoneIndex); + const FPhysAssetCreateParams& NewBodyData = GetDefault()->CreateParams; + // Create the physics body. - NewBodyIndex = FPhysicsAssetUtils::CreateNewBody(PhysicsAsset, NewBoneName); + NewBodyIndex = FPhysicsAssetUtils::CreateNewBody(PhysicsAsset, NewBoneName, NewBodyData); UBodySetup* BodySetup = PhysicsAsset->SkeletalBodySetups[ NewBodyIndex ]; check(BodySetup->BoneName == NewBoneName); @@ -999,7 +1013,6 @@ void FPhysicsAssetEditorSharedData::MakeNewBody(int32 NewBoneIndex, bool bAutoSe bool bCreatedBody = false; // Create a new physics body for this bone. - const FPhysAssetCreateParams& NewBodyData = GetDefault()->CreateParams; if (NewBodyData.VertWeight == EVW_DominantWeight) { bCreatedBody = FPhysicsAssetUtils::CreateCollisionFromBone(BodySetup, EditorSkelMesh, NewBoneIndex, NewBodyData, DominantWeightBoneInfos[NewBoneIndex]); diff --git a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletalMeshComponent.cpp b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletalMeshComponent.cpp index 52b8551dd379..717b67cc271e 100644 --- a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletalMeshComponent.cpp +++ b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletalMeshComponent.cpp @@ -104,6 +104,10 @@ void UPhysicsAssetEditorSkeletalMeshComponent::RenderAssetTools(const FSceneView // Draw bodies for (int32 i = 0; i SkeletalBodySetups.Num(); ++i) { + if (!ensure(PhysicsAsset->SkeletalBodySetups[i])) + { + continue; + } int32 BoneIndex = GetBoneIndex(PhysicsAsset->SkeletalBodySetups[i]->BoneName); // If we found a bone for it, draw the collision. diff --git a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletonTreeBuilder.cpp b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletonTreeBuilder.cpp index a205fc771de7..652654ff7c34 100644 --- a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletonTreeBuilder.cpp +++ b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetEditorSkeletonTreeBuilder.cpp @@ -99,6 +99,10 @@ void FPhysicsAssetEditorSkeletonTreeBuilder::AddBodies(FSkeletonTreeBuilderOutpu bool bHasBodySetup = false; for (int32 BodySetupIndex = 0; BodySetupIndex < PhysicsAsset->SkeletalBodySetups.Num(); ++BodySetupIndex) { + if (!ensure(PhysicsAsset->SkeletalBodySetups[BodySetupIndex])) + { + continue; + } if (BoneName == PhysicsAsset->SkeletalBodySetups[BodySetupIndex]->BoneName) { USkeletalBodySetup* BodySetup = PhysicsAsset->SkeletalBodySetups[BodySetupIndex]; diff --git a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetGraph/PhysicsAssetGraph.cpp b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetGraph/PhysicsAssetGraph.cpp index dc5a80f3f336..226505a5da07 100644 --- a/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetGraph/PhysicsAssetGraph.cpp +++ b/Engine/Source/Editor/PhysicsAssetEditor/Private/PhysicsAssetGraph/PhysicsAssetGraph.cpp @@ -134,7 +134,7 @@ void UPhysicsAssetGraph::SelectObjects(const TArray& InBodi for (int32 BodyIndex = 0; BodyIndex < PhysicsAsset->SkeletalBodySetups.Num(); ++BodyIndex) { USkeletalBodySetup* CheckBodySetup = PhysicsAsset->SkeletalBodySetups[BodyIndex]; - if (CheckBodySetup->BoneName == ConstraintInstance.ConstraintBone1 || CheckBodySetup->BoneName == ConstraintInstance.ConstraintBone2) + if (CheckBodySetup && (CheckBodySetup->BoneName == ConstraintInstance.ConstraintBone1 || CheckBodySetup->BoneName == ConstraintInstance.ConstraintBone2)) { SelectedBodies.AddUnique(CheckBodySetup); break; diff --git a/Engine/Source/Editor/PropertyEditor/Private/DetailPropertyRow.cpp b/Engine/Source/Editor/PropertyEditor/Private/DetailPropertyRow.cpp index 04f4a01940cc..a124b118dcc7 100644 --- a/Engine/Source/Editor/PropertyEditor/Private/DetailPropertyRow.cpp +++ b/Engine/Source/Editor/PropertyEditor/Private/DetailPropertyRow.cpp @@ -194,6 +194,13 @@ FDetailWidgetRow FDetailPropertyRow::GetWidgetRow() MakeNameOrKeyWidget( Row, CustomPropertyWidget ); MakeValueWidget( Row, CustomPropertyWidget ); + if (CustomPropertyWidget.IsValid()) + { + Row.CopyMenuAction = CustomPropertyWidget->CopyMenuAction; + Row.PasteMenuAction = CustomPropertyWidget->PasteMenuAction; + Row.CustomMenuItems = CustomPropertyWidget->CustomMenuItems; + } + return Row; } else diff --git a/Engine/Source/Editor/PropertyEditor/Private/Presentation/PropertyTable/PropertyTableColumn.cpp b/Engine/Source/Editor/PropertyEditor/Private/Presentation/PropertyTable/PropertyTableColumn.cpp index ba0b3d8cd384..05862d93466b 100644 --- a/Engine/Source/Editor/PropertyEditor/Private/Presentation/PropertyTable/PropertyTableColumn.cpp +++ b/Engine/Source/Editor/PropertyEditor/Private/Presentation/PropertyTable/PropertyTableColumn.cpp @@ -136,6 +136,79 @@ private: const FCompareRowByColumnAscending< UPropertyType > Comparer; }; +struct FCompareRowByColumnUsingExportTextLexicographic : public FCompareRowByColumnBase +{ +public: + FCompareRowByColumnUsingExportTextLexicographic( const TSharedRef< IPropertyTableColumn >& InColumn, const UProperty* InUProperty, bool InAscendingOrder ) + : Property( InUProperty ) + , Column( InColumn ) + , bAscending( InAscendingOrder ) + { + + } + + int32 Compare( const TSharedRef< IPropertyTableRow >& Lhs, const TSharedRef< IPropertyTableRow >& Rhs ) const + { + const TSharedRef< IPropertyTableCell > LhsCell = Column->GetCell( Lhs ); + const TSharedRef< IPropertyTableCell > RhsCell = Column->GetCell( Rhs ); + + const TSharedPtr< FPropertyNode > LhsPropertyNode = LhsCell->GetNode(); + if ( !LhsPropertyNode.IsValid() ) + { + return 1; + } + + const TSharedPtr< FPropertyNode > RhsPropertyNode = RhsCell->GetNode(); + if ( !RhsPropertyNode.IsValid() ) + { + return -1; + } + + const TSharedPtr< IPropertyHandle > LhsPropertyHandle = PropertyEditorHelpers::GetPropertyHandle( LhsPropertyNode.ToSharedRef(), NULL, NULL ); + if ( !LhsPropertyHandle.IsValid() ) + { + return 1; + } + + const TSharedPtr< IPropertyHandle > RhsPropertyHandle = PropertyEditorHelpers::GetPropertyHandle( RhsPropertyNode.ToSharedRef(), NULL, NULL ); + if ( !RhsPropertyHandle.IsValid() ) + { + return -1; + } + + return ComparePropertyValue( LhsPropertyHandle, RhsPropertyHandle ); + } + +private: + + int32 ComparePropertyValue( const TSharedPtr< IPropertyHandle >& LhsPropertyHandle, const TSharedPtr< IPropertyHandle >& RhsPropertyHandle ) const + { + FString LhsValue; + LhsPropertyHandle->GetValueAsDisplayString( LhsValue ); + + FString RhsValue; + RhsPropertyHandle->GetValueAsDisplayString( RhsValue ); + + if (LhsValue < RhsValue) + { + return bAscending ? -1 : 1; + } + else if (LhsValue > RhsValue) + { + return bAscending ? 1: -1; + } + + return 0; + } + +private: + + const UProperty* Property; + TSharedRef< IPropertyTableColumn > Column; + bool bAscending; +}; + + template<> FORCEINLINE int32 FCompareRowByColumnAscending::ComparePropertyValue( const TSharedPtr< IPropertyHandle >& LhsPropertyHandle, const TSharedPtr< IPropertyHandle >& RhsPropertyHandle ) const @@ -508,21 +581,7 @@ bool FPropertyTableColumn::CanSortBy() const Property = Path->GetLeafMostProperty().Property.Get(); } - if ( Property != NULL ) - { - return Property->IsA( UByteProperty::StaticClass() ) || - Property->IsA( UIntProperty::StaticClass() ) || - Property->IsA( UBoolProperty::StaticClass() ) || - Property->IsA( UEnumProperty::StaticClass() ) || - Property->IsA( UFloatProperty::StaticClass() ) || - Property->IsA( UNameProperty::StaticClass() ) || - Property->IsA( UStrProperty::StaticClass() ) || - IsSupportedStructProperty( Property ) || - ( Property->IsA( UObjectPropertyBase::StaticClass() ) && !Property->HasAnyPropertyFlags(CPF_InstancedReference) ); - //Property->IsA( UTextProperty::StaticClass() ); - } - - return false; + return ( Property != nullptr ); } TSharedPtr FPropertyTableColumn::GetPropertySorter(UProperty* Property, EColumnSortMode::Type SortMode) @@ -618,7 +677,7 @@ TSharedPtr FPropertyTableColumn::GetPropertySorter(UPro return MakeShareable(new FCompareRowByColumnDescending(SharedThis(this), StrProperty)); } } - else if (Property->IsA(UObjectPropertyBase::StaticClass())) + else if (Property->IsA(UObjectPropertyBase::StaticClass()) && !Property->HasAnyPropertyFlags(CPF_InstancedReference)) { UObjectPropertyBase* ObjectProperty = Cast(Property); @@ -657,8 +716,7 @@ TSharedPtr FPropertyTableColumn::GetPropertySorter(UPro //} else { - check(false && "Cannot Sort Rows by this Column!"); - return nullptr; + return MakeShareable(new FCompareRowByColumnUsingExportTextLexicographic(SharedThis(this), Property, (SortMode == EColumnSortMode::Ascending))); } } diff --git a/Engine/Source/Editor/PropertyEditor/Private/PropertyHandleImpl.cpp b/Engine/Source/Editor/PropertyEditor/Private/PropertyHandleImpl.cpp index 38d37d128a93..162ff0f6d5c7 100644 --- a/Engine/Source/Editor/PropertyEditor/Private/PropertyHandleImpl.cpp +++ b/Engine/Source/Editor/PropertyEditor/Private/PropertyHandleImpl.cpp @@ -27,6 +27,30 @@ #define LOCTEXT_NAMESPACE "PropertyHandleImplementation" +void PropertyToTextHelper(FString& OutString, FPropertyNode* InPropertyNode, UProperty* Property, uint8* ValueAddress, EPropertyPortFlags PortFlags) +{ + if (InPropertyNode->GetArrayIndex() != INDEX_NONE || Property->ArrayDim == 1) + { + Property->ExportText_Direct(OutString, ValueAddress, ValueAddress, nullptr, PortFlags); + } + else + { + UArrayProperty::ExportTextInnerItem(OutString, Property, ValueAddress, Property->ArrayDim, ValueAddress, Property->ArrayDim, nullptr, PortFlags); + } +} + +void TextToPropertyHelper(const TCHAR* Buffer, FPropertyNode* InPropertyNode, UProperty* Property, uint8* ValueAddress, UObject* Object) +{ + if (InPropertyNode->GetArrayIndex() != INDEX_NONE || Property->ArrayDim == 1) + { + Property->ImportText(Buffer, ValueAddress, 0, Object); + } + else + { + UArrayProperty::ImportTextInnerItem(Buffer, Property, ValueAddress, 0, Object); + } +} + FPropertyValueImpl::FPropertyValueImpl( TSharedPtr InPropertyNode, FNotifyHook* InNotifyHook, TSharedPtr InPropertyUtilities ) : PropertyNode( InPropertyNode ) , PropertyUtilities( InPropertyUtilities ) @@ -88,7 +112,7 @@ FPropertyAccess::Result FPropertyValueImpl::GetPropertyValueString( FString& Out // Check for bogus data if( Property != nullptr && InPropertyNode->GetParentNode() != nullptr ) { - Property->ExportText_Direct( OutString, ValueAddress, ValueAddress, nullptr, PortFlags ); + PropertyToTextHelper(OutString, InPropertyNode, Property, ValueAddress, PortFlags); UEnum* Enum = nullptr; int64 EnumValue = 0; @@ -161,7 +185,7 @@ FPropertyAccess::Result FPropertyValueImpl::GetPropertyValueText( FText& OutText else { FString ExportedTextString; - Property->ExportText_Direct(ExportedTextString, ValueAddress, ValueAddress, nullptr, PPF_PropertyWindow ); + PropertyToTextHelper(ExportedTextString, InPropertyNode, Property, ValueAddress, PPF_PropertyWindow); UEnum* Enum = nullptr; int64 EnumValue = 0; @@ -404,7 +428,7 @@ FPropertyAccess::Result FPropertyValueImpl::ImportText( const TArrayExportText_Direct(PreviousValue, Cur.BaseAddress, Cur.BaseAddress, nullptr, 0 ); + PropertyToTextHelper(PreviousValue, InPropertyNode, NodeProperty, Cur.BaseAddress, PPF_None); // If this property is the inner-property of a container, cache the current value as well FString PreviousContainerValue; @@ -491,13 +515,13 @@ FPropertyAccess::Result FPropertyValueImpl::ImportText( const TArrayImportText( NewValue, Cur.BaseAddress, 0, Cur.Object ); + TextToPropertyHelper(NewValue, InPropertyNode, NodeProperty, Cur.BaseAddress, Cur.Object); if (Cur.Object) { // Cache the value of the property after having modified it. FString ValueAfterImport; - NodeProperty->ExportText_Direct(ValueAfterImport, Cur.BaseAddress, Cur.BaseAddress, nullptr, 0); + PropertyToTextHelper(ValueAfterImport, InPropertyNode, NodeProperty, Cur.BaseAddress, PPF_None); if ((Cur.Object->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject) || (Cur.Object->HasAnyFlags(RF_DefaultSubObject) && Cur.Object->GetOuter()->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))) && diff --git a/Engine/Source/Editor/PropertyEditor/Private/PropertyNode.cpp b/Engine/Source/Editor/PropertyEditor/Private/PropertyNode.cpp index f7cce7f60e55..b071cc97a35e 100644 --- a/Engine/Source/Editor/PropertyEditor/Private/PropertyNode.cpp +++ b/Engine/Source/Editor/PropertyEditor/Private/PropertyNode.cpp @@ -1707,17 +1707,8 @@ FString FPropertyNode::GetDefaultValueAsStringForObject( FPropertyItemValueDataT } else if ( GetArrayIndex() == INDEX_NONE && InProperty->ArrayDim > 1 ) { - for ( int32 Idx = 0; !bDiffersFromDefaultForObject && Idx < InProperty->ArrayDim; Idx++ ) - { - uint8* DefaultAddress = ValueTracker.GetPropertyDefaultAddress() + Idx * InProperty->ElementSize; - FString DefaultItem; - InProperty->ExportTextItem( DefaultItem, DefaultAddress, DefaultAddress, InObject, PortFlags, NULL ); - if ( DefaultValue.Len() > 0 && DefaultItem.Len() > 0 ) - { - DefaultValue += TEXT( ", " ); - } - DefaultValue += DefaultItem; - } + UArrayProperty::ExportTextInnerItem(DefaultValue, InProperty, ValueTracker.GetPropertyDefaultAddress(), InProperty->ArrayDim, + ValueTracker.GetPropertyDefaultAddress(), InProperty->ArrayDim, nullptr, PortFlags); } else { diff --git a/Engine/Source/Editor/PropertyEditor/Private/SDetailSingleItemRow.cpp b/Engine/Source/Editor/PropertyEditor/Private/SDetailSingleItemRow.cpp index cd4ad56145ae..744bfe433e22 100644 --- a/Engine/Source/Editor/PropertyEditor/Private/SDetailSingleItemRow.cpp +++ b/Engine/Source/Editor/PropertyEditor/Private/SDetailSingleItemRow.cpp @@ -532,15 +532,26 @@ bool SDetailSingleItemRow::OnContextMenuOpening(FMenuBuilder& MenuBuilder) bAddedMenuEntry = true; } - for(const FDetailWidgetRow::FCustomMenuData& CustomMenuData : Customization->GetWidgetRow().CustomMenuItems) + const TArray& CustomMenuActions = Customization->GetWidgetRow().CustomMenuItems; + if (CustomMenuActions.Num() > 0) { - //Add the menu entry - MenuBuilder.AddMenuEntry( - CustomMenuData.Name, - CustomMenuData.Tooltip, - CustomMenuData.SlateIcon, - CustomMenuData.Action); - bAddedMenuEntry = true; + // Hide separator line if it only contains the SearchWidget, making the next 2 elements the top of the list + if (MenuBuilder.GetMultiBox()->GetBlocks().Num() > 1) + { + MenuBuilder.AddMenuSeparator(); + } + + for (const FDetailWidgetRow::FCustomMenuData& CustomMenuData : CustomMenuActions) + { + //Add the menu entry + MenuBuilder.AddMenuEntry( + CustomMenuData.Name, + CustomMenuData.Tooltip, + CustomMenuData.SlateIcon, + CustomMenuData.Action); + bAddedMenuEntry = true; + } + } return bAddedMenuEntry; diff --git a/Engine/Source/Editor/Sequencer/Private/Sequencer.cpp b/Engine/Source/Editor/Sequencer/Private/Sequencer.cpp index 38714bd45ae9..69172f99c9b4 100644 --- a/Engine/Source/Editor/Sequencer/Private/Sequencer.cpp +++ b/Engine/Source/Editor/Sequencer/Private/Sequencer.cpp @@ -5704,7 +5704,7 @@ void FSequencer::AddActorsToBinding(FGuid InObjectBinding, const TArray { if (!ObjectsInCurrentSequence.Contains(ActorToAdd)) { - if (ActorClass == nullptr || ActorToAdd->GetClass() == ActorClass) + if (ActorClass == nullptr || UClass::FindCommonBase(ActorToAdd->GetClass(), ActorClass) != nullptr) { if (ActorClass == nullptr) { diff --git a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.cpp b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.cpp index 62c8c4768229..bd4321eb4adb 100644 --- a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.cpp +++ b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.cpp @@ -41,6 +41,8 @@ #include "LODUtilities.h" #include "ScopedTransaction.h" #include "ComponentReregisterContext.h" +#include "EditorFramework/AssetImportData.h" +#include "Factories/FbxSkeletalMeshImportData.h" const FName SkeletalMeshEditorAppIdentifier = FName(TEXT("SkeletalMeshEditorApp")); @@ -162,10 +164,10 @@ void FSkeletalMeshEditor::BindCommands() FSkeletalMeshEditorCommands::Register(); ToolkitCommands->MapAction(FSkeletalMeshEditorCommands::Get().ReimportMesh, - FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportMesh)); + FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportMesh, (int32)INDEX_NONE)); ToolkitCommands->MapAction(FSkeletalMeshEditorCommands::Get().ReimportAllMesh, - FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportAllMesh)); + FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportAllMesh, (int32)INDEX_NONE)); ToolkitCommands->MapAction(FSkeletalMeshEditorCommands::Get().MeshSectionSelection, FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::ToggleMeshSectionSelection), @@ -189,15 +191,164 @@ void FSkeletalMeshEditor::ExtendToolbar() auto ConstructReimportContextMenu = [this]() { + bool bShowSubMenu = SkeletalMesh != nullptr && SkeletalMesh->AssetImportData != nullptr && SkeletalMesh->AssetImportData->GetSourceFileCount() > 1; FMenuBuilder MenuBuilder(true, nullptr); - MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportMesh->GetLabel(), - FSkeletalMeshEditorCommands::Get().ReimportMesh->GetDescription(), - FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportMesh))); - MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetLabel(), - FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetDescription(), - FSlateIcon(), - FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportAllMesh))); + + if (!bShowSubMenu) + { + //Reimport + MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportMesh->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportMesh->GetDescription(), + FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportMesh, 0))); + + MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportMeshWithNewFile->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportMeshWithNewFile->GetDescription(), + FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportMeshWithNewFile, 0))); + + //Reimport ALL + MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetDescription(), + FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportAllMesh, 0))); + + MenuBuilder.AddMenuEntry(FSkeletalMeshEditorCommands::Get().ReimportAllMeshWithNewFile->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportAllMeshWithNewFile->GetDescription(), + FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &FSkeletalMeshEditor::HandleReimportAllMeshWithNewFile, 0))); + + FText ReimportMultiSources = LOCTEXT("ReimportMultiSources", "Reimport Content"); + FText ReimportMultiSourcesTooltip = LOCTEXT("ReimportMultiSourcesTooltip", "Reimport Geometry or Skinning Weights content, this will create multi import source file."); + + auto CreateMultiContentSubMenu = [this](FMenuBuilder& SubMenuBuilder) + { + auto UpdateImportDataContentType = [this](int32 SourceFileIndex) + { + UFbxSkeletalMeshImportData* SkeletalMeshImportData = Cast(SkeletalMesh->AssetImportData); + if (SkeletalMeshImportData) + { + SkeletalMeshImportData->ImportContentType = SourceFileIndex == 0 ? EFBXImportContentType::FBXICT_All : SourceFileIndex == 1 ? EFBXImportContentType::FBXICT_Geometry : EFBXImportContentType::FBXICT_SkinningWeights; + HandleReimportMeshWithNewFile(SourceFileIndex); + } + }; + + FText ReimportGeometryContentLabel = LOCTEXT("ReimportGeometryContentLabel", "Geometry"); + FText ReimportGeometryContentLabelTooltip = LOCTEXT("ReimportGeometryContentLabelTooltipTooltip", "Reimport Geometry Only"); + SubMenuBuilder.AddMenuEntry( + ReimportGeometryContentLabel, + ReimportGeometryContentLabelTooltip, + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateLambda(UpdateImportDataContentType, 1), + FCanExecuteAction() + ) + ); + FText ReimportSkinningAndWeightsContentLabel = LOCTEXT("ReimportSkinningAndWeightsContentLabel", "Skinning And Weights"); + FText ReimportSkinningAndWeightsContentLabelTooltip = LOCTEXT("ReimportSkinningAndWeightsContentLabelTooltipTooltip", "Reimport Skinning And Weights Only"); + SubMenuBuilder.AddMenuEntry( + ReimportSkinningAndWeightsContentLabel, + ReimportSkinningAndWeightsContentLabelTooltip, + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateLambda(UpdateImportDataContentType, 2), + FCanExecuteAction() + ) + ); + }; + + MenuBuilder.AddSubMenu( + ReimportMultiSources, + ReimportMultiSourcesTooltip, + FNewMenuDelegate::CreateLambda(CreateMultiContentSubMenu)); + } + else + { + auto CreateSubMenu = [this](FMenuBuilder& SubMenuBuilder, bool bReimportAll, bool bWithNewFile) + { + //Get the data, we cannot use the closure since the lambda will be call when the function scope will be gone + TArray SourceFilePaths; + SkeletalMesh->AssetImportData->ExtractFilenames(SourceFilePaths); + TArray SourceFileLabels; + SkeletalMesh->AssetImportData->ExtractDisplayLabels(SourceFileLabels); + + if (SourceFileLabels.Num() > 0 && SourceFileLabels.Num() == SourceFilePaths.Num()) + { + auto UpdateImportDataContentType = [this](int32 SourceFileIndex, bool bReimportAll, bool bWithNewFile) + { + UFbxSkeletalMeshImportData* SkeletalMeshImportData = Cast(SkeletalMesh->AssetImportData); + if (SkeletalMeshImportData) + { + SkeletalMeshImportData->ImportContentType = SourceFileIndex == 0 ? EFBXImportContentType::FBXICT_All : SourceFileIndex == 1 ? EFBXImportContentType::FBXICT_Geometry : EFBXImportContentType::FBXICT_SkinningWeights; + if (bReimportAll) + { + if (bWithNewFile) + { + HandleReimportAllMeshWithNewFile(SourceFileIndex); + } + else + { + HandleReimportAllMesh(SourceFileIndex); + } + } + else + { + if (bWithNewFile) + { + HandleReimportMeshWithNewFile(SourceFileIndex); + } + else + { + HandleReimportMesh(SourceFileIndex); + } + } + } + }; + + for (int32 SourceFileIndex = 0; SourceFileIndex < SourceFileLabels.Num(); ++SourceFileIndex) + { + FText ReimportLabel = FText::Format(LOCTEXT("ReimportNoLabel", "SourceFile {0}"), SourceFileIndex); + FText ReimportLabelTooltip = FText::Format(LOCTEXT("ReimportNoLabelTooltip", "Reimport File: {0}"), FText::FromString(SourceFilePaths[SourceFileIndex])); + if (SourceFileLabels[SourceFileIndex].Len() > 0) + { + ReimportLabel = FText::Format(LOCTEXT("ReimportLabel", "{0}"), FText::FromString(SourceFileLabels[SourceFileIndex])); + ReimportLabelTooltip = FText::Format(LOCTEXT("ReimportLabelTooltip", "Reimport {0} File: {1}"), FText::FromString(SourceFileLabels[SourceFileIndex]), FText::FromString(SourceFilePaths[SourceFileIndex])); + } + SubMenuBuilder.AddMenuEntry( + ReimportLabel, + ReimportLabelTooltip, + FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.AssetActions.ReimportAsset"), + FUIAction( + FExecuteAction::CreateLambda(UpdateImportDataContentType, SourceFileIndex, bReimportAll, bWithNewFile), + FCanExecuteAction() + ) + ); + } + } + }; + + //Create 4 submenu: Reimport, ReimportWithNewFile, ReimportAll and ReimportAllWithNewFile + MenuBuilder.AddSubMenu( + FSkeletalMeshEditorCommands::Get().ReimportMesh->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportMesh->GetDescription(), + FNewMenuDelegate::CreateLambda(CreateSubMenu, false, false)); + + MenuBuilder.AddSubMenu( + FSkeletalMeshEditorCommands::Get().ReimportMeshWithNewFile->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportMeshWithNewFile->GetDescription(), + FNewMenuDelegate::CreateLambda(CreateSubMenu, false, true)); + + MenuBuilder.AddSubMenu( + FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportAllMesh->GetDescription(), + FNewMenuDelegate::CreateLambda(CreateSubMenu, true, false)); + + MenuBuilder.AddSubMenu( + FSkeletalMeshEditorCommands::Get().ReimportAllMeshWithNewFile->GetLabel(), + FSkeletalMeshEditorCommands::Get().ReimportAllMeshWithNewFile->GetDescription(), + FNewMenuDelegate::CreateLambda(CreateSubMenu, true, true)); + } + return MenuBuilder.MakeWidget(); }; @@ -729,44 +880,94 @@ UObject* FSkeletalMeshEditor::HandleGetAsset() return GetEditingObject(); } -void FSkeletalMeshEditor::HandleReimportMesh() +bool FSkeletalMeshEditor::HandleReimportMeshInternal(int32 SourceFileIndex /*= INDEX_NONE*/, bool bWithNewFile /*= false*/) { // Reimport the asset - if (SkeletalMesh) + return FReimportManager::Instance()->Reimport(SkeletalMesh, true, true, TEXT(""), nullptr, SourceFileIndex, bWithNewFile); +} + +void FSkeletalMeshEditor::HandleReimportMesh(int32 SourceFileIndex /*= INDEX_NONE*/) +{ + HandleReimportMeshInternal(SourceFileIndex, false); +} + +void FSkeletalMeshEditor::HandleReimportMeshWithNewFile(int32 SourceFileIndex /*= INDEX_NONE*/) +{ + HandleReimportMeshInternal(SourceFileIndex, true); +} + +void ReimportAllCustomLODs(USkeletalMesh* SkeletalMesh, UDebugSkelMeshComponent* PreviewMeshComponent, bool bWithNewFile) +{ + //Find the dependencies of the generated LOD + TArray Dependencies; + Dependencies.AddZeroed(SkeletalMesh->GetLODNum()); + //Avoid making LOD 0 to true in the dependencies since everything that should be regenerate base on LOD 0 is already regenerate at this point. + //But we need to regenerate every generated LOD base on any re-import custom LOD + //Reimport all custom LODs + for (int32 LodIndex = 1; LodIndex < SkeletalMesh->GetLODNum(); ++LodIndex) { - FReimportManager::Instance()->Reimport(SkeletalMesh, true); + //Do not reimport LOD that was re-import with the base mesh + if (SkeletalMesh->GetLODInfo(LodIndex)->bImportWithBaseMesh) + { + continue; + } + if (SkeletalMesh->GetLODInfo(LodIndex)->bHasBeenSimplified == false) + { + FString SourceFilenameBackup = SkeletalMesh->GetLODInfo(LodIndex)->SourceImportFilename; + if (bWithNewFile) + { + SkeletalMesh->GetLODInfo(LodIndex)->SourceImportFilename.Empty(); + } + + if (!FbxMeshUtils::ImportMeshLODDialog(SkeletalMesh, LodIndex)) + { + if (bWithNewFile) + { + SkeletalMesh->GetLODInfo(LodIndex)->SourceImportFilename = SourceFilenameBackup; + } + } + else + { + Dependencies[LodIndex] = true; + } + } + else if(Dependencies[SkeletalMesh->GetLODInfo(LodIndex)->ReductionSettings.BaseLOD]) + { + //Regenerate the LOD + FSkeletalMeshUpdateContext UpdateContext; + UpdateContext.SkeletalMesh = SkeletalMesh; + UpdateContext.AssociatedComponents.Push(PreviewMeshComponent); + FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LodIndex); + Dependencies[LodIndex] = true; + } } } -void FSkeletalMeshEditor::HandleReimportAllMesh() +void FSkeletalMeshEditor::HandleReimportAllMesh(int32 SourceFileIndex /*= INDEX_NONE*/) { // Reimport the asset if (SkeletalMesh) { //Reimport base LOD - if (FReimportManager::Instance()->Reimport(SkeletalMesh, true)) + if (HandleReimportMeshInternal(SourceFileIndex, false)) { //Reimport all custom LODs - for (int32 LodIndex = 1; LodIndex < SkeletalMesh->GetLODNum(); ++LodIndex) - { - //Do not reimport LOD that was re-import with the base mesh - if (SkeletalMesh->GetLODInfo(LodIndex)->bImportWithBaseMesh) - { - continue; - } - if (SkeletalMesh->GetLODInfo(LodIndex)->bHasBeenSimplified == false) - { - FbxMeshUtils::ImportMeshLODDialog(SkeletalMesh, LodIndex); - } - else - { - //Regenerate the LOD - FSkeletalMeshUpdateContext UpdateContext; - UpdateContext.SkeletalMesh = SkeletalMesh; - UpdateContext.AssociatedComponents.Push(GetPersonaToolkit()->GetPreviewMeshComponent()); - FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LodIndex); - } - } + ReimportAllCustomLODs(SkeletalMesh, GetPersonaToolkit()->GetPreviewMeshComponent(), false); + } + } +} + +void FSkeletalMeshEditor::HandleReimportAllMeshWithNewFile(int32 SourceFileIndex /*= INDEX_NONE*/) +{ + // Reimport the asset + if (SkeletalMesh) + { + TArray ImportObjs; + ImportObjs.Add(SkeletalMesh); + if (HandleReimportMeshInternal(SourceFileIndex, true)) + { + //Reimport all custom LODs + ReimportAllCustomLODs(SkeletalMesh, GetPersonaToolkit()->GetPreviewMeshComponent(), true); } } } diff --git a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.h b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.h index abd2150d62e0..294842ee4b66 100644 --- a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.h +++ b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditor.h @@ -96,8 +96,12 @@ private: void HandleSelectionChanged(const TArrayView>& InSelectedItems, ESelectInfo::Type InSelectInfo); - void HandleReimportMesh(); - void HandleReimportAllMesh(); + void HandleReimportMesh(int32 SourceFileIndex = INDEX_NONE); + void HandleReimportMeshWithNewFile(int32 SourceFileIndex = INDEX_NONE); + + bool HandleReimportMeshInternal(int32 SourceFileIndex = INDEX_NONE, bool bWithNewFile = false); + void HandleReimportAllMesh(int32 SourceFileIndex = INDEX_NONE); + void HandleReimportAllMeshWithNewFile(int32 SourceFileIndex = INDEX_NONE); /** Callback for toggling UV drawing in the viewport */ void ToggleMeshSectionSelection(); diff --git a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.cpp b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.cpp index 7171fd0e649d..eca608f481f5 100644 --- a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.cpp +++ b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.cpp @@ -8,7 +8,9 @@ void FSkeletalMeshEditorCommands::RegisterCommands() { UI_COMMAND(ReimportMesh, "Reimport Mesh", "Reimport the current mesh.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ReimportMeshWithNewFile, "Reimport Mesh With New File", "Reimport the current mesh using a new source file.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(ReimportAllMesh, "Reimport All Mesh", "Reimport the current mesh and all the custom LODs.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ReimportAllMeshWithNewFile, "Reimport All Mesh With New File", "Reimport the current mesh using a new source file and all the custom LODs (No new source file for LODs).", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(MeshSectionSelection, "Section Selection", "Enables selecting Mesh Sections in the viewport (disables selecting bones using their physics shape).", EUserInterfaceActionType::ToggleButton, FInputChord()); } diff --git a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.h b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.h index 2d522975d56e..a5bbb9f7968c 100644 --- a/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.h +++ b/Engine/Source/Editor/SkeletalMeshEditor/Private/SkeletalMeshEditorCommands.h @@ -20,9 +20,11 @@ public: // reimport current mesh TSharedPtr ReimportMesh; + TSharedPtr ReimportMeshWithNewFile; // reimport current mesh TSharedPtr ReimportAllMesh; + TSharedPtr ReimportAllMeshWithNewFile; // selecting mesh section using hit proxies TSharedPtr MeshSectionSelection; diff --git a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditor.cpp b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditor.cpp index 16b8efc2f8dc..b7e180c3ebd6 100644 --- a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditor.cpp +++ b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditor.cpp @@ -782,9 +782,7 @@ void FStaticMeshEditor::SetSelectedSocket(UStaticMeshSocket* InSelectedSocket) UStaticMeshSocket* FStaticMeshEditor::GetSelectedSocket() const { - check(SocketManager.IsValid()); - - return SocketManager->GetSelectedSocket(); + return SocketManager.IsValid() ? SocketManager->GetSelectedSocket() : nullptr; } void FStaticMeshEditor::DuplicateSelectedSocket() @@ -1375,9 +1373,8 @@ void FStaticMeshEditor::HandleReimportAllMesh() { continue; } - - - bool bHasBeenSimplified = !StaticMesh->IsMeshDescriptionValid(LodIndex) || StaticMesh->IsReductionActive(LodIndex); + + bool bHasBeenSimplified = StaticMesh->GetMeshDescription(LodIndex) == nullptr || StaticMesh->IsReductionActive(LodIndex); if (!bHasBeenSimplified) { FbxMeshUtils::ImportMeshLODDialog(StaticMesh, LodIndex); diff --git a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorActions.cpp b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorActions.cpp index 8608f3adb9c7..8f11b9ade75f 100644 --- a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorActions.cpp +++ b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorActions.cpp @@ -18,7 +18,9 @@ void FStaticMeshEditorCommands::RegisterCommands() UI_COMMAND( SetDrawAdditionalData, "Additional Data", "Draw additional user data associated with asset.", EUserInterfaceActionType::ToggleButton, FInputChord() ); UI_COMMAND( ReimportMesh, "Reimport Mesh", "Reimport the current mesh.", EUserInterfaceActionType::Button, FInputChord() ); - UI_COMMAND( ReimportAllMesh, "Reimport All Mesh", "Reimport the current mesh and all the custom LODs.", EUserInterfaceActionType::Button, FInputChord() ); + UI_COMMAND( ReimportMeshWithNewFile, "Reimport Mesh With New File", "Reimport the current mesh using a new source file.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND( ReimportAllMesh, "Reimport All Mesh", "Reimport the current mesh and all the custom LODs.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND( ReimportAllMeshWithNewFile, "Reimport All Mesh With New File", "Reimport the current mesh using a new source file and all the custom LODs (No new source file for the custom LODs).", EUserInterfaceActionType::Button, FInputChord() ); UI_COMMAND( SetShowNormals, "Normals", "Toggles display of vertex normals in the Preview Pane.", EUserInterfaceActionType::ToggleButton, FInputChord() ); UI_COMMAND( SetShowTangents, "Tangents", "Toggles display of vertex tangents in the Preview Pane.", EUserInterfaceActionType::ToggleButton, FInputChord() ); diff --git a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.cpp b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.cpp index 0051f302c3dc..e6bd3745a5c6 100644 --- a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.cpp +++ b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.cpp @@ -40,6 +40,10 @@ #include "Widgets/Input/SFilePathPicker.h" #include "EditorDirectories.h" #include "EditorFramework/AssetImportData.h" +#include "Factories/FbxStaticMeshImportData.h" +#include "Interfaces/ITargetPlatformManagerModule.h" +#include "Interfaces/ITargetPlatform.h" + const uint32 MaxHullCount = 64; const uint32 MinHullCount = 2; @@ -1014,7 +1018,7 @@ void FMeshBuildSettingsLayout::OnDistanceFieldResolutionScaleCommitted(float New FMeshReductionSettingsLayout::FMeshReductionSettingsLayout( TSharedRef InParentLODSettings, int32 InCurrentLODIndex, bool InCanReduceMyself) : ParentLODSettings( InParentLODSettings ) - , CurrentLODIndex( InCurrentLODIndex ) + , CurrentLODIndex(InCurrentLODIndex) , bCanReduceMyself(InCanReduceMyself) { @@ -1326,7 +1330,7 @@ void FMeshReductionSettingsLayout::GenerateChildContent( IDetailChildrenBuilder& //Base LOD { int32 MaxBaseReduceIndex = bCanReduceMyself ? CurrentLODIndex : CurrentLODIndex - 1; - ChildrenBuilder.AddCustomRow( LOCTEXT("ReductionBaseLOD", "Base LOD") ) + ChildrenBuilder.AddCustomRow(LOCTEXT("ReductionBaseLOD", "Base LOD")) .NameContent() .HAlign(HAlign_Left) [ @@ -3220,7 +3224,7 @@ void FLevelOfDetailSettingsLayout::AddLODLevelCategories( IDetailLayoutBuilder& if (IsAutoMeshReductionAvailable()) { - ReductionSettingsWidgets[LODIndex] = MakeShareable( new FMeshReductionSettingsLayout( AsShared(), LODIndex, StaticMesh->IsMeshDescriptionValid(LODIndex))); + ReductionSettingsWidgets[LODIndex] = MakeShareable( new FMeshReductionSettingsLayout(AsShared(), LODIndex, StaticMesh->IsMeshDescriptionValid(LODIndex))); } if (LODIndex < StaticMesh->SourceModels.Num()) @@ -3260,7 +3264,7 @@ void FLevelOfDetailSettingsLayout::AddLODLevelCategories( IDetailLayoutBuilder& CategoryName.AppendInt( LODIndex ); FText LODLevelString = FText::FromString(FString(TEXT("LOD ")) + FString::FromInt(LODIndex) ); - bool bHasBeenSimplified = !StaticMesh->IsMeshDescriptionValid(LODIndex) || StaticMesh->IsReductionActive(LODIndex); + bool bHasBeenSimplified = StaticMesh->GetMeshDescription(LODIndex) == nullptr || StaticMesh->IsReductionActive(LODIndex); FText GeneratedString = FText::FromString(bHasBeenSimplified ? TEXT("[generated]") : TEXT("")); IDetailCategoryBuilder& LODCategory = DetailBuilder.EditCategory( *CategoryName, LODLevelString, ECategoryPriority::Important ); @@ -3748,17 +3752,48 @@ void FLevelOfDetailSettingsLayout::OnImportLOD(TSharedPtr NewValue, ESe UStaticMesh* StaticMesh = StaticMeshEditor.GetStaticMesh(); check(StaticMesh); + if (StaticMesh->LODGroup != NAME_None && StaticMesh->SourceModels.IsValidIndex(LODIndex)) + { + // Cache derived data for the running platform. + ITargetPlatformManagerModule& TargetPlatformManager = GetTargetPlatformManagerRef(); + ITargetPlatform* RunningPlatform = TargetPlatformManager.GetRunningTargetPlatform(); + check(RunningPlatform); + const FStaticMeshLODSettings& LODSettings = RunningPlatform->GetStaticMeshLODSettings(); + const FStaticMeshLODGroup& LODGroup = LODSettings.GetLODGroup(StaticMesh->LODGroup); + if (LODIndex < LODGroup.GetDefaultNumLODs()) + { + //Ask the user to change the LODGroup to None, if the user cancel do not re-import the LOD + //We can have a LODGroup with custom LOD only if custom LOD are after the generated LODGroup LODs + EAppReturnType::Type ReturnResult = FMessageDialog::Open(EAppMsgType::OkCancel, EAppReturnType::Ok, FText::Format(LOCTEXT("LODImport_LODGroupVersusCustomLODConflict", + "This static mesh uses the LOD group \"{0}\" which generates the LOD {1}. To import a custom LOD at index {1}, the LODGroup must be cleared to \"None\"."), FText::FromName(StaticMesh->LODGroup), FText::AsNumber(LODIndex))); + if (ReturnResult == EAppReturnType::Cancel) + { + StaticMeshEditor.RefreshTool(); + return; + } + //Clear the LODGroup + StaticMesh->SetLODGroup(NAME_None, false); + //Make sure the importdata point on LOD Group None + UFbxStaticMeshImportData* ImportData = Cast(StaticMesh->AssetImportData); + if (ImportData != nullptr) + { + ImportData->StaticMeshLODGroup = NAME_None; + } + } + } + //Are we a new imported LOD, we want to set some value for new imported LOD. //This boolean prevent changing the value when the LOD is reimport bool bImportCustomLOD = (LODIndex >= StaticMesh->SourceModels.Num()); - - bool bResult = FbxMeshUtils::ImportMeshLODDialog(StaticMesh,LODIndex); + + bool bResult = FbxMeshUtils::ImportMeshLODDialog(StaticMesh, LODIndex); if (bImportCustomLOD && bResult && StaticMesh->SourceModels.IsValidIndex(LODIndex)) { //Custom LOD should reduce base on them self when they get imported. StaticMesh->SourceModels[LODIndex].ReductionSettings.BaseLODModel = LODIndex; } + StaticMesh->PostEditChange(); StaticMeshEditor.RefreshTool(); } diff --git a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.h b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.h index a0bce0876d31..23b7fde70b5e 100644 --- a/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.h +++ b/Engine/Source/Editor/StaticMeshEditor/Private/StaticMeshEditorTools.h @@ -215,7 +215,7 @@ private: class FMeshReductionSettingsLayout : public IDetailCustomNodeBuilder, public TSharedFromThis { public: - FMeshReductionSettingsLayout( TSharedRef InParentLODSettings, int32 InCurrentLODIndex, bool InCanReduceMyself); + FMeshReductionSettingsLayout(TSharedRef InParentLODSettings, int32 InCurrentLODIndex, bool InCanReduceMyself); virtual ~FMeshReductionSettingsLayout(); const FMeshReductionSettings& GetSettings() const; diff --git a/Engine/Source/Editor/StaticMeshEditor/Public/StaticMeshEditorActions.h b/Engine/Source/Editor/StaticMeshEditor/Public/StaticMeshEditorActions.h index 8eedabdc5781..6abf83c517ca 100644 --- a/Engine/Source/Editor/StaticMeshEditor/Public/StaticMeshEditorActions.h +++ b/Engine/Source/Editor/StaticMeshEditor/Public/StaticMeshEditorActions.h @@ -43,7 +43,9 @@ public: // Mesh toolbar Commands TSharedPtr< FUICommandInfo > ReimportMesh; + TSharedPtr< FUICommandInfo > ReimportMeshWithNewFile; TSharedPtr< FUICommandInfo > ReimportAllMesh; + TSharedPtr< FUICommandInfo > ReimportAllMeshWithNewFile; // View Menu Commands TSharedPtr< FUICommandInfo > SetShowNormals; diff --git a/Engine/Source/Editor/StatsViewer/Private/StatsPages/PrimitiveStatsPage.cpp b/Engine/Source/Editor/StatsViewer/Private/StatsPages/PrimitiveStatsPage.cpp index e77375267d90..c39b46405092 100644 --- a/Engine/Source/Editor/StatsViewer/Private/StatsPages/PrimitiveStatsPage.cpp +++ b/Engine/Source/Editor/StatsViewer/Private/StatsPages/PrimitiveStatsPage.cpp @@ -352,11 +352,12 @@ struct PrimitiveStatsGenerator NewStatsEntry->Sections += FMath::Square(CurrentComponent->NumSubsections); // count resource usage of landscape + //TODO: take into consideration all the editing RT/heightmap, etc. bool bNotUnique = false; - UniqueTextures.Add(CurrentComponent->HeightmapTexture, &bNotUnique); + UniqueTextures.Add(CurrentComponent->GetHeightmap(), &bNotUnique); if (!bNotUnique) { - const SIZE_T HeightmapResourceSize = CurrentComponent->HeightmapTexture->GetResourceSizeBytes(EResourceSizeMode::EstimatedTotal); + const SIZE_T HeightmapResourceSize = CurrentComponent->GetHeightmap()->GetResourceSizeBytes(EResourceSizeMode::EstimatedTotal); NewStatsEntry->ResourceSize += (float)HeightmapResourceSize / 1024.0f; } if (CurrentComponent->XYOffsetmapTexture) diff --git a/Engine/Source/Editor/UMGEditor/Classes/K2Node_WidgetAnimationEvent.h b/Engine/Source/Editor/UMGEditor/Classes/K2Node_WidgetAnimationEvent.h index cf17422a3a15..cadae9541b39 100644 --- a/Engine/Source/Editor/UMGEditor/Classes/K2Node_WidgetAnimationEvent.h +++ b/Engine/Source/Editor/UMGEditor/Classes/K2Node_WidgetAnimationEvent.h @@ -64,10 +64,6 @@ private: void Initialize(const UWidgetBlueprint* InSourceBlueprint, UWidgetAnimation* InAnimation, EWidgetAnimationEvent InAction); private: - /** Cached display name for the delegate property */ - UPROPERTY() - FText DelegatePropertyDisplayName; - /** Constructing FText strings can be costly, so we cache the node's title/tooltip */ FNodeTextCache CachedTooltip; FNodeTextCache CachedNodeTitle; diff --git a/Engine/Source/Editor/UMGEditor/Classes/WidgetGraphSchema.h b/Engine/Source/Editor/UMGEditor/Classes/WidgetGraphSchema.h index b9285b45e74e..3fe95e8fcc34 100644 --- a/Engine/Source/Editor/UMGEditor/Classes/WidgetGraphSchema.h +++ b/Engine/Source/Editor/UMGEditor/Classes/WidgetGraphSchema.h @@ -7,6 +7,9 @@ #include "EdGraphSchema_K2.h" #include "WidgetGraphSchema.generated.h" +class UK2Node; +class UK2Node_CallFunction; + UCLASS(MinimalAPI) class UWidgetGraphSchema : public UEdGraphSchema_K2 { @@ -21,4 +24,8 @@ private: void ConvertAddAnimationDelegate(UEdGraph* Graph) const; void ConvertRemoveAnimationDelegate(UEdGraph* Graph) const; void ConvertClearAnimationDelegate(UEdGraph* Graph) const; + + void ReplaceAnimationFunctionAndAllocateDefaultPins(UEdGraph* Graph, UK2Node* OldNode, UK2Node_CallFunction* NewFunctionNode) const; + + void FixDefaultToSelfForAnimation(UEdGraph* Graph) const; }; diff --git a/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.cpp b/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.cpp index a84c56fa6da3..445a327635df 100644 --- a/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.cpp +++ b/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.cpp @@ -12,8 +12,13 @@ #include "DetailWidgetRow.h" #include "DetailLayoutBuilder.h" #include "ScopedTransaction.h" +#include "SFunctionSelector.h" +#include "Framework/Application/SlateApplication.h" +#include "Blueprint/WidgetTree.h" +#include "WidgetBlueprint.h" +#include "WidgetBlueprintEditor.h" -#define LOCTEXT_NAMESPACE "UMG" +#define LOCTEXT_NAMESPACE "FWidgetNavigationCustomization" // FWidgetNavigationCustomization //////////////////////////////////////////////////////////////////////////////// @@ -92,6 +97,17 @@ FText FWidgetNavigationCustomization::GetNavigationText(TWeakPtr PropertyHandle, EUINavigation Nav) const +{ + TOptional OptionalName = GetUniformNavigationTargetOrFunction(PropertyHandle, Nav); + if (!OptionalName.IsSet()) + { + return LOCTEXT("NavigationMultipleValues", "Multiple Values"); + } + + return FText::FromName(OptionalName.GetValue()); +} + +TOptional FWidgetNavigationCustomization::GetUniformNavigationTargetOrFunction(TWeakPtr PropertyHandle, EUINavigation Nav) const { TArray OuterObjects; TSharedPtr PropertyHandlePtr = PropertyHandle.Pin(); @@ -117,17 +133,17 @@ FText FWidgetNavigationCustomization::GetExplictWidget(TWeakPtr if ( CurRule != Rule ) { - return LOCTEXT("NavigationMultipleValues", "Multiple Values"); + return TOptional(); } Rule = CurRule; } } - return FText::FromName(Rule); + return Rule; } -void FWidgetNavigationCustomization::OnCommitExplictWidgetText(const FText& ItemFText, ETextCommit::Type CommitInfo, TWeakPtr PropertyHandle, EUINavigation Nav) +void FWidgetNavigationCustomization::OnWidgetSelectedForExplicitNavigation(FName ExplictWidgetOrFunction, TWeakPtr PropertyHandle, EUINavigation Nav) { TArray OuterObjects; TSharedPtr PropertyHandlePtr = PropertyHandle.Pin(); @@ -135,16 +151,14 @@ void FWidgetNavigationCustomization::OnCommitExplictWidgetText(const FText& Item const FScopedTransaction Transaction(LOCTEXT("InitializeNavigation", "Edit Widget Navigation")); - FName GotoWidgetName = FName(*ItemFText.ToString()); - for ( UObject* OuterObject : OuterObjects ) { if ( UWidget* Widget = Cast(OuterObject) ) { FWidgetReference WidgetReference = Editor.Pin()->GetReferenceFromPreview(Widget); - SetNav(WidgetReference.GetPreview(), Nav, TOptional(), GotoWidgetName); - SetNav(WidgetReference.GetTemplate(), Nav, TOptional(), GotoWidgetName); + SetNav(WidgetReference.GetPreview(), Nav, TOptional(), ExplictWidgetOrFunction); + SetNav(WidgetReference.GetTemplate(), Nav, TOptional(), ExplictWidgetOrFunction); } } } @@ -155,10 +169,20 @@ EVisibility FWidgetNavigationCustomization::GetExplictWidgetFieldVisibility(TWea switch (Rule) { case EUINavigationRule::Explicit: - case EUINavigationRule::Custom: - case EUINavigationRule::CustomBoundary: return EVisibility::Visible; - + } + + return EVisibility::Collapsed; +} + +EVisibility FWidgetNavigationCustomization::GetCustomWidgetFieldVisibility(TWeakPtr PropertyHandle, EUINavigation Nav) const +{ + EUINavigationRule Rule = GetNavigationRule(PropertyHandle, Nav); + switch (Rule) + { + case EUINavigationRule::Custom: + case EUINavigationRule::CustomBoundary: + return EVisibility::Visible; } return EVisibility::Collapsed; @@ -166,6 +190,8 @@ EVisibility FWidgetNavigationCustomization::GetExplictWidgetFieldVisibility(TWea void FWidgetNavigationCustomization::MakeNavRow(TWeakPtr PropertyHandle, IDetailChildrenBuilder& ChildBuilder, EUINavigation Nav, FText NavName) { + static UFunction* CustomWidgetNavSignature = FindObject(FindPackage(nullptr, TEXT("/Script/UMG")), TEXT("CustomWidgetNavigationDelegate__DelegateSignature")); + ChildBuilder.AddCustomRow(NavName) .NameContent() [ @@ -199,13 +225,95 @@ void FWidgetNavigationCustomization::MakeNavRow(TWeakPtr Proper + SHorizontalBox::Slot() .FillWidth(1.0f) [ - SNew(SEditableTextBox) - .HintText(LOCTEXT("WidgetName", "Widget Name?")) - .Text(this, &FWidgetNavigationCustomization::GetExplictWidget, PropertyHandle, Nav) - .OnTextCommitted(this, &FWidgetNavigationCustomization::OnCommitExplictWidgetText, PropertyHandle, Nav) - .Font(IDetailLayoutBuilder::GetDetailFont()) + SNew(SComboButton) .Visibility(this, &FWidgetNavigationCustomization::GetExplictWidgetFieldVisibility, PropertyHandle, Nav) + .OnGetMenuContent(this, &FWidgetNavigationCustomization::OnGenerateWidgetList, PropertyHandle, Nav) + .ContentPadding(1) + .ButtonContent() + [ + SNew(STextBlock) + .Text(this, &FWidgetNavigationCustomization::GetExplictWidget, PropertyHandle, Nav) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] ] + + // Custom Navigation Widget + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SFunctionSelector, Editor.Pin().ToSharedRef(), CustomWidgetNavSignature) + .OnSelectedFunction(this, &FWidgetNavigationCustomization::HandleSelectedCustomNavigationFunction, PropertyHandle, Nav) + .OnResetFunction(this, &FWidgetNavigationCustomization::HandleResetCustomNavigationFunction, PropertyHandle, Nav) + .CurrentFunction(this, &FWidgetNavigationCustomization::GetUniformNavigationTargetOrFunction, PropertyHandle, Nav) + .Visibility(this, &FWidgetNavigationCustomization::GetCustomWidgetFieldVisibility, PropertyHandle, Nav) + ] + ]; +} + +void FWidgetNavigationCustomization::HandleSelectedCustomNavigationFunction(FName SelectedFunction, TWeakPtr PropertyHandle, EUINavigation Nav) +{ + OnWidgetSelectedForExplicitNavigation(SelectedFunction, PropertyHandle, Nav); +} + +void FWidgetNavigationCustomization::HandleResetCustomNavigationFunction(TWeakPtr PropertyHandle, EUINavigation Nav) +{ + OnWidgetSelectedForExplicitNavigation(NAME_None, PropertyHandle, Nav); +} + +TSharedRef FWidgetNavigationCustomization::OnGenerateWidgetList(TWeakPtr PropertyHandle, EUINavigation Nav) +{ + const bool bInShouldCloseWindowAfterMenuSelection = true; + FMenuBuilder MenuBuilder(bInShouldCloseWindowAfterMenuSelection, nullptr); + + TArray Widgets; + + UWidgetTree* WidgetTree = Editor.Pin()->GetWidgetBlueprintObj()->WidgetTree; + WidgetTree->GetAllWidgets(Widgets); + + Widgets.Sort([](const UWidget& LHS, const UWidget& RHS) { + return LHS.GetName() > RHS.GetName(); + }); + + { + MenuBuilder.BeginSection("Actions"); + { + MenuBuilder.AddMenuEntry( + LOCTEXT("ResetFunction", "Reset"), + LOCTEXT("ResetFunctionTooltip", "Reset this navigation option and clear it out."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "Cross"), + FUIAction(FExecuteAction::CreateSP(this, &FWidgetNavigationCustomization::HandleResetCustomNavigationFunction, PropertyHandle, Nav)) + ); + } + MenuBuilder.EndSection(); + } + + MenuBuilder.BeginSection("Widgets", LOCTEXT("Widgets", "Widgets")); + { + for (UWidget* Widget : Widgets) + { + if (!Widget->IsGeneratedName()) + { + MenuBuilder.AddMenuEntry( + FText::FromString(Widget->GetDisplayLabel()), + FText::GetEmpty(), + FSlateIcon(), + FUIAction(FExecuteAction::CreateSP(this, &FWidgetNavigationCustomization::OnWidgetSelectedForExplicitNavigation, Widget->GetFName(), PropertyHandle, Nav)) + ); + } + } + } + MenuBuilder.EndSection(); + + FDisplayMetrics DisplayMetrics; + FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); + + return + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .MaxHeight(DisplayMetrics.PrimaryDisplayHeight * 0.5) + [ + MenuBuilder.MakeWidget() ]; } @@ -251,8 +359,8 @@ void FWidgetNavigationCustomization::HandleNavMenuEntryClicked(TWeakPtrGetReferenceFromPreview(Widget); - SetNav(WidgetReference.GetPreview(), Nav, Rule, TOptional()); - SetNav(WidgetReference.GetTemplate(), Nav, Rule, TOptional()); + SetNav(WidgetReference.GetPreview(), Nav, Rule, FName(NAME_None)); + SetNav(WidgetReference.GetTemplate(), Nav, Rule, FName(NAME_None)); } } } @@ -270,6 +378,7 @@ void FWidgetNavigationCustomization::SetNav(UWidget* Widget, EUINavigation Nav, if (!Widget->Navigation) { WidgetNavigation = NewObject(Widget); + WidgetNavigation->SetFlags(RF_Transactional); } FWidgetNavigationData* DirectionNavigation = nullptr; diff --git a/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.h b/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.h index a93107f15bb3..58e828a3b470 100644 --- a/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.h +++ b/Engine/Source/Editor/UMGEditor/Private/Customizations/WidgetNavigationCustomization.h @@ -39,8 +39,15 @@ private: void HandleNavMenuEntryClicked(TWeakPtr PropertyHandle, EUINavigation Nav, EUINavigationRule Rule); + TSharedRef OnGenerateWidgetList(TWeakPtr PropertyHandle, EUINavigation Nav); + + void HandleSelectedCustomNavigationFunction(FName SelectedFunction, TWeakPtr PropertyHandle, EUINavigation Nav); + void HandleResetCustomNavigationFunction(TWeakPtr PropertyHandle, EUINavigation Nav); + FText GetExplictWidget(TWeakPtr PropertyHandle, EUINavigation Nav) const; - void OnCommitExplictWidgetText(const FText& ItemFText, ETextCommit::Type CommitInfo, TWeakPtr PropertyHandle, EUINavigation Nav); + TOptional GetUniformNavigationTargetOrFunction(TWeakPtr PropertyHandle, EUINavigation Nav) const; + void OnWidgetSelectedForExplicitNavigation(FName ExplictWidgetOrFunction, TWeakPtr PropertyHandle, EUINavigation Nav); + EVisibility GetCustomWidgetFieldVisibility(TWeakPtr PropertyHandle, EUINavigation Nav) const; EVisibility GetExplictWidgetFieldVisibility(TWeakPtr PropertyHandle, EUINavigation Nav) const; void SetNav(UWidget* Widget, EUINavigation Nav, TOptional Rule, TOptional WidgetToFocus); diff --git a/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.cpp b/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.cpp new file mode 100644 index 000000000000..d3a54b848d4b --- /dev/null +++ b/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.cpp @@ -0,0 +1,312 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "Details/SFunctionSelector.h" +#include "Framework/Application/SlateApplication.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Text/STextBlock.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SComboButton.h" + +#if WITH_EDITOR + #include "Components/PrimitiveComponent.h" + #include "Components/StaticMeshComponent.h" + #include "Engine/BlueprintGeneratedClass.h" +#endif // WITH_EDITOR +#include "EdGraph/EdGraph.h" +#include "EdGraphSchema_K2.h" +#include "Blueprint/WidgetBlueprintGeneratedClass.h" +#include "Animation/WidgetAnimation.h" +#include "WidgetBlueprint.h" + +#include "DetailLayoutBuilder.h" +#include "BlueprintModes/WidgetBlueprintApplicationModes.h" +#include "WidgetGraphSchema.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "ScopedTransaction.h" +#include "Components/WidgetComponent.h" +#include "Binding/PropertyBinding.h" + + +#define LOCTEXT_NAMESPACE "SFunctionSelector" + + +///////////////////////////////////////////////////// +// SFunctionSelector + +void SFunctionSelector::Construct(const FArguments& InArgs, TSharedRef InEditor, UFunction* InAllowedSignature) +{ + Editor = InEditor; + Blueprint = InEditor->GetWidgetBlueprintObj(); + + CurrentFunction = InArgs._CurrentFunction; + SelectedFunctionEvent = InArgs._OnSelectedFunction; + ResetFunctionEvent = InArgs._OnResetFunction; + + BindableSignature = InAllowedSignature; + + ChildSlot + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SComboButton) + .OnGetMenuContent(this, &SFunctionSelector::OnGenerateDelegateMenu) + .ContentPadding(1) + .ButtonContent() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 1, 0, 0) + [ + SNew(STextBlock) + .Text(this, &SFunctionSelector::GetCurrentBindingText) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ] + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .Visibility(this, &SFunctionSelector::GetGotoBindingVisibility) + .OnClicked(this, &SFunctionSelector::HandleGotoBindingClicked) + .VAlign(VAlign_Center) + .ToolTipText(LOCTEXT("GotoFunction", "Goto Function")) + [ + SNew(SImage) + .Image(FEditorStyle::GetBrush("PropertyWindow.Button_Browse")) + ] + ] + ]; +} + +template +void SFunctionSelector::ForEachBindableFunction(UClass* FromClass, Predicate Pred) const +{ + // Walk up class hierarchy for native functions and properties + for ( TFieldIterator FuncIt(FromClass, EFieldIteratorFlags::IncludeSuper); FuncIt; ++FuncIt ) + { + UFunction* Function = *FuncIt; + + // Only bind to functions that are callable from blueprints + if ( !UEdGraphSchema_K2::CanUserKismetCallFunction(Function) ) + { + continue; + } + + // We ignore CPF_ReturnParm because all that matters for binding to script functions is that the number of out parameters match. + if ( Function->IsSignatureCompatibleWith(BindableSignature, UFunction::GetDefaultIgnoredSignatureCompatibilityFlags() | CPF_ReturnParm) ) + { + TSharedPtr Info = MakeShareable(new FFunctionInfo()); + Info->DisplayName = FText::FromName(Function->GetFName()); + Info->Tooltip = Function->GetMetaData("Tooltip"); + Info->FuncName = Function->GetFName(); + + Pred(Info); + } + } +} + +TSharedRef SFunctionSelector::OnGenerateDelegateMenu() +{ + const bool bInShouldCloseWindowAfterMenuSelection = true; + FMenuBuilder MenuBuilder(bInShouldCloseWindowAfterMenuSelection, nullptr); + + MenuBuilder.BeginSection("BindingActions"); + { + if ( CanReset() ) + { + MenuBuilder.AddMenuEntry( + LOCTEXT("ResetFunction", "Reset"), + LOCTEXT("ResetFunctionTooltip", "Reset this function and clear it out."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "Cross"), + FUIAction(FExecuteAction::CreateSP(this, &SFunctionSelector::HandleRemoveBinding)) + ); + } + + MenuBuilder.AddMenuEntry( + LOCTEXT("CreateFunction", "Create Function"), + LOCTEXT("CreateBindingToolTip", "Creates a new function"), + FSlateIcon(FEditorStyle::GetStyleSetName(), "Plus"), + FUIAction(FExecuteAction::CreateSP(this, &SFunctionSelector::HandleCreateAndAddBinding)) + ); + } + MenuBuilder.EndSection(); //CreateBinding + + // Bindable options + { + // Get the current skeleton class, think header for the blueprint. + UBlueprintGeneratedClass* SkeletonClass = Cast(Blueprint->SkeletonGeneratedClass); + + FillPropertyMenu(MenuBuilder, SkeletonClass); + } + + FDisplayMetrics DisplayMetrics; + FSlateApplication::Get().GetCachedDisplayMetrics(DisplayMetrics); + + return + SNew(SVerticalBox) + + + SVerticalBox::Slot() + .MaxHeight(DisplayMetrics.PrimaryDisplayHeight * 0.5) + [ + MenuBuilder.MakeWidget() + ]; +} + +void SFunctionSelector::FillPropertyMenu(FMenuBuilder& MenuBuilder, UStruct* OwnerStruct) +{ + bool bFoundEntry = false; + + //--------------------------------------- + // Function Bindings + + if ( UClass* OwnerClass = Cast(OwnerStruct) ) + { + static FName FunctionIcon(TEXT("GraphEditor.Function_16x")); + + MenuBuilder.BeginSection("Functions", LOCTEXT("Functions", "Functions")); + { + ForEachBindableFunction(OwnerClass, [&] (TSharedPtr Info) { + bFoundEntry = true; + + MenuBuilder.AddMenuEntry( + Info->DisplayName, + FText::FromString(Info->Tooltip), + FSlateIcon(FEditorStyle::GetStyleSetName(), FunctionIcon), + FUIAction(FExecuteAction::CreateSP(this, &SFunctionSelector::HandleAddFunctionBinding, Info)) + ); + }); + } + MenuBuilder.EndSection(); //Functions + } + + // Get the current skeleton class, think header for the blueprint. + UBlueprintGeneratedClass* SkeletonClass = Cast(Blueprint->GeneratedClass); + + if ( bFoundEntry == false && OwnerStruct != SkeletonClass ) + { + MenuBuilder.BeginSection("None", OwnerStruct->GetDisplayNameText()); + MenuBuilder.AddWidget(SNew(STextBlock).Text(LOCTEXT("None", "None")), FText::GetEmpty()); + MenuBuilder.EndSection(); //None + } +} + +FText SFunctionSelector::GetCurrentBindingText() const +{ + TOptional Current = CurrentFunction.Get(); + + if (Current.IsSet()) + { + if (Current.GetValue() == NAME_None) + { + return LOCTEXT("SelectFunction", "Select Function"); + } + else + { + return FText::FromName(Current.GetValue()); + } + } + + return LOCTEXT("MultipleValues", "Multiple Values"); +} + +bool SFunctionSelector::CanReset() +{ + TOptional Current = CurrentFunction.Get(); + if (Current.IsSet()) + { + return Current.GetValue() != NAME_None; + } + + return true; +} + +void SFunctionSelector::HandleRemoveBinding() +{ + ResetFunctionEvent.ExecuteIfBound(); +} + +void SFunctionSelector::HandleAddFunctionBinding(TSharedPtr SelectedFunction) +{ + SelectedFunctionEvent.ExecuteIfBound(SelectedFunction->FuncName); +} + +void SFunctionSelector::HandleCreateAndAddBinding() +{ + const FScopedTransaction Transaction(LOCTEXT("CreateDelegate", "Create Binding")); + + Blueprint->Modify(); + + // Create the function graph. + FString FunctionName = TEXT("DoCustomNavigation"); // TODO Change this to make it generic and exposed as a parameter. + UEdGraph* FunctionGraph = FBlueprintEditorUtils::CreateNewGraph( + Blueprint, + FBlueprintEditorUtils::FindUniqueKismetName(Blueprint, FunctionName), + UEdGraph::StaticClass(), + UEdGraphSchema_K2::StaticClass()); + + // Add the binding to the blueprint + TSharedPtr SelectedFunction = MakeShareable(new FFunctionInfo()); + SelectedFunction->FuncName = FunctionGraph->GetFName(); + + HandleAddFunctionBinding(SelectedFunction); + + const bool bUserCreated = true; + FBlueprintEditorUtils::AddFunctionGraph(Blueprint, FunctionGraph, bUserCreated, BindableSignature); + + GotoFunction(FunctionGraph); +} + +EVisibility SFunctionSelector::GetGotoBindingVisibility() const +{ + TOptional Current = CurrentFunction.Get(); + if (Current.IsSet()) + { + if (Current.GetValue() != NAME_None) + { + return EVisibility::Visible; + } + } + + return EVisibility::Collapsed; +} + +FReply SFunctionSelector::HandleGotoBindingClicked() +{ + TOptional Current = CurrentFunction.Get(); + if (ensure(Current.IsSet())) + { + if (ensure(Current.GetValue() != NAME_None)) + { + TArray AllGraphs; + Blueprint->GetAllGraphs(AllGraphs); + + for (UEdGraph* Graph : AllGraphs) + { + if ( Graph->GetFName() == Current) + { + GotoFunction(Graph); + } + } + } + } + + return FReply::Handled(); +} + +void SFunctionSelector::GotoFunction(UEdGraph* FunctionGraph) +{ + Editor.Pin()->SetCurrentMode(FWidgetBlueprintApplicationModes::GraphMode); + Editor.Pin()->OpenDocument(FunctionGraph, FDocumentTracker::OpenNewDocument); +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.h b/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.h new file mode 100644 index 000000000000..6dd89bc79ac2 --- /dev/null +++ b/Engine/Source/Editor/UMGEditor/Private/Details/SFunctionSelector.h @@ -0,0 +1,81 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Layout/Visibility.h" +#include "Input/Reply.h" +#include "Widgets/SWidget.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "WidgetBlueprintEditor.h" +#include "EdGraph/EdGraphSchema.h" +#include "PropertyHandle.h" + +class FMenuBuilder; +class UEdGraph; +class UWidgetBlueprint; +struct FEditorPropertyPath; + +class SFunctionSelector : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_OneParam(FFunctionDelegate, FName /*SelectedFunctionName*/); + DECLARE_DELEGATE(FResetDelegate); + + SLATE_BEGIN_ARGS(SFunctionSelector) + {} + + SLATE_ATTRIBUTE(TOptional, CurrentFunction) + + SLATE_EVENT(FFunctionDelegate, OnSelectedFunction) + SLATE_EVENT(FResetDelegate, OnResetFunction) + + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, TSharedRef InEditor, UFunction* InAllowedSignature); + +protected: + struct FFunctionInfo + { + FFunctionInfo() + { + } + + FName FuncName; + FText DisplayName; + FString Tooltip; + }; + + TSharedRef OnGenerateDelegateMenu(); + void FillPropertyMenu(FMenuBuilder& MenuBuilder, UStruct* OwnerStruct); + + FText GetCurrentBindingText() const; + + bool CanReset(); + void HandleRemoveBinding(); + + void HandleAddFunctionBinding(TSharedPtr SelectedFunction); + + void HandleCreateAndAddBinding(); + void GotoFunction(UEdGraph* FunctionGraph); + + EVisibility GetGotoBindingVisibility() const; + + FReply HandleGotoBindingClicked(); + +private: + + template + void ForEachBindableFunction(UClass* FromClass, Predicate Pred) const; + + TAttribute> CurrentFunction; + + FFunctionDelegate SelectedFunctionEvent; + FResetDelegate ResetFunctionEvent; + + TWeakPtr Editor; + UWidgetBlueprint* Blueprint; + + UFunction* BindableSignature; +}; diff --git a/Engine/Source/Editor/UMGEditor/Private/Details/SPropertyBinding.h b/Engine/Source/Editor/UMGEditor/Private/Details/SPropertyBinding.h index 8f00aa026138..5cd6e88e7731 100644 --- a/Engine/Source/Editor/UMGEditor/Private/Details/SPropertyBinding.h +++ b/Engine/Source/Editor/UMGEditor/Private/Details/SPropertyBinding.h @@ -64,8 +64,6 @@ protected: FReply HandleGotoBindingClicked(TSharedRef PropertyHandle); - FReply AddOrViewEventBinding(TSharedPtr Action); - private: template diff --git a/Engine/Source/Editor/UMGEditor/Private/K2Node_WidgetAnimationEvent.cpp b/Engine/Source/Editor/UMGEditor/Private/K2Node_WidgetAnimationEvent.cpp index 99ddcc437168..bb89bf722087 100644 --- a/Engine/Source/Editor/UMGEditor/Private/K2Node_WidgetAnimationEvent.cpp +++ b/Engine/Source/Editor/UMGEditor/Private/K2Node_WidgetAnimationEvent.cpp @@ -28,7 +28,6 @@ void UK2Node_WidgetAnimationEvent::Initialize(const UWidgetBlueprint* InSourceBl AnimationPropertyName = InAnimation->GetMovieScene()->GetFName(); Action = InAction; - DelegatePropertyDisplayName = InAnimation->GetDisplayName(); MarkDirty(); } diff --git a/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprint.cpp b/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprint.cpp index 892c5c1b4409..a6d05a5b3ac3 100644 --- a/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprint.cpp +++ b/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprint.cpp @@ -30,9 +30,12 @@ #include "K2Node_CallFunction.h" #include "K2Node_MacroInstance.h" #include "K2Node_Composite.h" +#include "Blueprint/WidgetNavigation.h" #define LOCTEXT_NAMESPACE "UMG" +FWidgetBlueprintDelegates::FGetAssetTags FWidgetBlueprintDelegates::GetAssetTags; + FEditorPropertyPathSegment::FEditorPropertyPathSegment() : Struct(nullptr) , MemberName(NAME_None) @@ -574,6 +577,32 @@ void UWidgetBlueprint::PreSave(const class ITargetPlatform* TargetPlatform) } #endif // WITH_EDITORONLY_DATA +#if WITH_EDITORONLY_DATA + +void UWidgetBlueprint::GetAssetRegistryTags(TArray& OutTags) const +{ + Super::GetAssetRegistryTags(OutTags); + + FWidgetBlueprintDelegates::GetAssetTags.Broadcast(this, OutTags); +} + +void UWidgetBlueprint::NotifyGraphRenamed(class UEdGraph* Graph, FName OldName, FName NewName) +{ + Super::NotifyGraphRenamed(Graph, OldName, NewName); + + // Update any explicit widget bindings. + WidgetTree->ForEachWidget([OldName, NewName](UWidget* Widget) { + if (Widget->Navigation) + { + Widget->Navigation->SetFlags(RF_Transactional); + Widget->Navigation->Modify(); + Widget->Navigation->TryToRenameBinding(OldName, NewName); + } + }); +} + +#endif + void UWidgetBlueprint::Serialize(FArchive& Ar) { Super::Serialize(Ar); @@ -592,7 +621,7 @@ void UWidgetBlueprint::PostLoad() if( GetLinkerUE4Version() < VER_UE4_FIXUP_WIDGET_ANIMATION_CLASS ) { - // Fixup widget animiations. + // Fixup widget animations. for( auto& OldAnim : AnimationData_DEPRECATED ) { FName AnimName = OldAnim.MovieScene->GetFName(); diff --git a/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprintEditorUtils.cpp b/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprintEditorUtils.cpp index 1df56ee36168..d4a3634fbaac 100644 --- a/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprintEditorUtils.cpp +++ b/Engine/Source/Editor/UMGEditor/Private/WidgetBlueprintEditorUtils.cpp @@ -32,6 +32,8 @@ #include "Utility/WidgetSlotPair.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" +#include "Components/Widget.h" +#include "Blueprint/WidgetNavigation.h" #define LOCTEXT_NAMESPACE "UMG" @@ -258,6 +260,16 @@ bool FWidgetBlueprintEditorUtils::RenameWidget(TSharedRefWidgetTree->ForEachWidget([OldObjectName, NewFName](UWidget* Widget) { + if (Widget->Navigation) + { + Widget->Navigation->SetFlags(RF_Transactional); + Widget->Navigation->Modify(); + Widget->Navigation->TryToRenameBinding(OldObjectName, NewFName); + } + }); + // Validate child blueprints and adjust variable names to avoid a potential name collision FBlueprintEditorUtils::ValidateBlueprintChildVariables(Blueprint, NewFName); diff --git a/Engine/Source/Editor/UMGEditor/Private/WidgetGraphSchema.cpp b/Engine/Source/Editor/UMGEditor/Private/WidgetGraphSchema.cpp index a92b2871dfa3..cd7f2498d9ba 100644 --- a/Engine/Source/Editor/UMGEditor/Private/WidgetGraphSchema.cpp +++ b/Engine/Source/Editor/UMGEditor/Private/WidgetGraphSchema.cpp @@ -9,6 +9,8 @@ #include "K2Node_WidgetAnimationEvent.h" #include "Animation/WidgetAnimation.h" #include "BlueprintNodeSpawner.h" +#include "K2Node_Self.h" +#include "UObject/FortniteMainBranchObjectVersion.h" UWidgetGraphSchema::UWidgetGraphSchema(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -34,11 +36,23 @@ void UWidgetGraphSchema::BackwardCompatibilityNodeConversion(UEdGraph* Graph, bo { if (Graph) { - ConvertAnimationEventNodes(Graph); + if (UWidgetBlueprint* WidgetBlueprint = Cast(Graph->GetOuter())) + { + const int32 WidgetBPVersion = WidgetBlueprint->GetLinkerCustomVersion(FFortniteMainBranchObjectVersion::GUID); - ConvertAddAnimationDelegate(Graph); - ConvertRemoveAnimationDelegate(Graph); - ConvertClearAnimationDelegate(Graph); + if (WidgetBPVersion < FFortniteMainBranchObjectVersion::WidgetStopDuplicatingAnimations) + { + ConvertAnimationEventNodes(Graph); + + ConvertAddAnimationDelegate(Graph); + ConvertRemoveAnimationDelegate(Graph); + ConvertClearAnimationDelegate(Graph); + } + else if (WidgetBPVersion < FFortniteMainBranchObjectVersion::WidgetAnimationDefaultToSelfFail) + { + FixDefaultToSelfForAnimation(Graph); + } + } } Super::BackwardCompatibilityNodeConversion(Graph, bOnlySafeChanges); @@ -85,22 +99,19 @@ void UWidgetGraphSchema::ConvertAddAnimationDelegate(UEdGraph* Graph) const IBlueprintNodeBinder::FBindingSet Bindings; UK2Node_CallFunction* CallFunction = Cast(CallFunctionSpawner->Invoke(Graph, Bindings, NodePos)); + TSubclassOf FunctionClass = Node->FindPinChecked(UEdGraphSchema_K2::PN_Self)->LinkedTo.Num() > 1 ? UWidgetAnimation::StaticClass() : UUserWidget::StaticClass(); + switch (GetAnimationEventFromDelegateName(Node->DelegateReference.GetMemberName())) { case EWidgetAnimationEvent::Started: - CallFunction->FunctionReference.SetExternalMember(TEXT("BindToAnimationStarted"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("BindToAnimationStarted"), FunctionClass); break; case EWidgetAnimationEvent::Finished: - CallFunction->FunctionReference.SetExternalMember(TEXT("BindToAnimationFinished"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("BindToAnimationFinished"), FunctionClass); break; } - CallFunction->AllocateDefaultPins(); - - TMap OldToNewPinMap; - //OldToNewPinMap.Add(UEdGraphSchema_K2::PN_Self, TEXT("Animation")); - OldToNewPinMap.Add(TEXT("Delegate"), TEXT("Delegate")); - ReplaceOldNodeWithNew(Node, CallFunction, OldToNewPinMap); + ReplaceAnimationFunctionAndAllocateDefaultPins(Graph, Node, CallFunction); } } } @@ -120,22 +131,19 @@ void UWidgetGraphSchema::ConvertRemoveAnimationDelegate(UEdGraph* Graph) const IBlueprintNodeBinder::FBindingSet Bindings; UK2Node_CallFunction* CallFunction = Cast(CallFunctionSpawner->Invoke(Graph, Bindings, NodePos)); + TSubclassOf FunctionClass = Node->FindPinChecked(UEdGraphSchema_K2::PN_Self)->LinkedTo.Num() > 1 ? UWidgetAnimation::StaticClass() : UUserWidget::StaticClass(); + switch (GetAnimationEventFromDelegateName(Node->DelegateReference.GetMemberName())) { case EWidgetAnimationEvent::Started: - CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindFromAnimationStarted"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindFromAnimationStarted"), FunctionClass); break; case EWidgetAnimationEvent::Finished: - CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindFromAnimationFinished"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindFromAnimationFinished"), FunctionClass); break; } - CallFunction->AllocateDefaultPins(); - - TMap OldToNewPinMap; - //OldToNewPinMap.Add(UEdGraphSchema_K2::PN_Self, TEXT("Animation")); - OldToNewPinMap.Add(TEXT("Delegate"), TEXT("Delegate")); - ReplaceOldNodeWithNew(Node, CallFunction, OldToNewPinMap); + ReplaceAnimationFunctionAndAllocateDefaultPins(Graph, Node, CallFunction); } } } @@ -155,21 +163,79 @@ void UWidgetGraphSchema::ConvertClearAnimationDelegate(UEdGraph* Graph) const IBlueprintNodeBinder::FBindingSet Bindings; UK2Node_CallFunction* CallFunction = Cast(CallFunctionSpawner->Invoke(Graph, Bindings, NodePos)); + TSubclassOf FunctionClass = Node->FindPinChecked(UEdGraphSchema_K2::PN_Self)->LinkedTo.Num() > 1 ? UWidgetAnimation::StaticClass() : UUserWidget::StaticClass(); + switch (GetAnimationEventFromDelegateName(Node->DelegateReference.GetMemberName())) { case EWidgetAnimationEvent::Started: - CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindAllFromAnimationStarted"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindAllFromAnimationStarted"), FunctionClass); break; case EWidgetAnimationEvent::Finished: - CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindAllFromAnimationFinished"), UWidgetAnimation::StaticClass()); + CallFunction->FunctionReference.SetExternalMember(TEXT("UnbindAllFromAnimationFinished"), FunctionClass); break; } - CallFunction->AllocateDefaultPins(); - - TMap OldToNewPinMap; - //OldToNewPinMap.Add(UEdGraphSchema_K2::PN_Self, TEXT("Animation")); - ReplaceOldNodeWithNew(Node, CallFunction, OldToNewPinMap); + ReplaceAnimationFunctionAndAllocateDefaultPins(Graph, Node, CallFunction); } } -} \ No newline at end of file +} + +void UWidgetGraphSchema::ReplaceAnimationFunctionAndAllocateDefaultPins(UEdGraph* Graph, UK2Node* OldNode, UK2Node_CallFunction* NewFunctionNode) const +{ + NewFunctionNode->AllocateDefaultPins(); + + TMap OldToNewPinMap; + if (NewFunctionNode->FindPin(TEXT("Animation"))) + { + OldToNewPinMap.Add(UEdGraphSchema_K2::PN_Self, TEXT("Animation")); + } + OldToNewPinMap.Add(TEXT("Delegate"), TEXT("Delegate")); + ReplaceOldNodeWithNew(OldNode, NewFunctionNode, OldToNewPinMap); + + UEdGraphPin* WidgetPin = NewFunctionNode->FindPin(TEXT("Widget"), EGPD_Input); + if (WidgetPin && WidgetPin->LinkedTo.Num() == 0) + { + FVector2D PinPos(NewFunctionNode->NodePosX - 200, NewFunctionNode->NodePosY + 128); + IBlueprintNodeBinder::FBindingSet Bindings; + UK2Node_Self* SelfNode = Cast(UBlueprintNodeSpawner::Create()->Invoke(Graph, Bindings, PinPos)); + + if (!TryCreateConnection(SelfNode->FindPinChecked(UEdGraphSchema_K2::PN_Self), WidgetPin)) + { + SelfNode->DestroyNode(); + } + } +} + +void UWidgetGraphSchema::FixDefaultToSelfForAnimation(UEdGraph* Graph) const +{ + TArray CallFunctionNodes; + Graph->GetNodesOfClass(CallFunctionNodes); + + TArray AnimationFunctionsToFix; + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("BindToAnimationStarted"))); + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("UnbindFromAnimationStarted"))); + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("UnbindAllFromAnimationStarted"))); + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("BindToAnimationFinished"))); + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("UnbindFromAnimationFinished"))); + AnimationFunctionsToFix.Add(UWidgetAnimation::StaticClass()->FindFunctionByName(TEXT("UnbindAllFromAnimationFinished"))); + + for (UK2Node_CallFunction* FunctionNode : CallFunctionNodes) + { + UFunction* Function = FunctionNode->GetTargetFunction(); + if (AnimationFunctionsToFix.Contains(Function)) + { + UEdGraphPin* WidgetPin = FunctionNode->FindPin(TEXT("Widget"), EGPD_Input); + if (WidgetPin && WidgetPin->LinkedTo.Num() == 0) + { + FVector2D PinPos(FunctionNode->NodePosX - 200, FunctionNode->NodePosY + 128); + IBlueprintNodeBinder::FBindingSet Bindings; + UK2Node_Self* SelfNode = Cast(UBlueprintNodeSpawner::Create()->Invoke(Graph, Bindings, PinPos)); + + if (!TryCreateConnection(SelfNode->FindPinChecked(UEdGraphSchema_K2::PN_Self), WidgetPin)) + { + SelfNode->DestroyNode(); + } + } + } + } +} diff --git a/Engine/Source/Editor/UMGEditor/Public/WidgetBlueprint.h b/Engine/Source/Editor/UMGEditor/Public/WidgetBlueprint.h index aa5aa14e4755..db0078f4dbff 100644 --- a/Engine/Source/Editor/UMGEditor/Public/WidgetBlueprint.h +++ b/Engine/Source/Editor/UMGEditor/Public/WidgetBlueprint.h @@ -20,10 +20,23 @@ class UUserWidget; class UWidget; class UWidgetAnimation; class FKismetCompilerContext; +class UWidgetBlueprint; enum class EWidgetTickFrequency : uint8; enum class EWidgetCompileTimeTickPrediction : uint8; +/** Widget Delegates */ +class UMGEDITOR_API FWidgetBlueprintDelegates +{ +public: + // delegate for generating widget asset registry tags. + DECLARE_MULTICAST_DELEGATE_TwoParams(FGetAssetTags, const UWidgetBlueprint*, TArray&); + + // called by UWdgetBlueprint::GetAssetRegistryTags() + static FGetAssetTags GetAssetTags; +}; + + /** */ USTRUCT() struct UMGEDITOR_API FEditorPropertyPathSegment @@ -254,9 +267,16 @@ public: /** UObject interface */ virtual void PostLoad() override; virtual void PostDuplicate(bool bDuplicateForPIE) override; + #if WITH_EDITORONLY_DATA virtual void PreSave(const class ITargetPlatform* TargetPlatform) override; #endif // WITH_EDITORONLY_DATA + +#if WITH_EDITOR + virtual void GetAssetRegistryTags(TArray& OutTags) const override; + virtual void NotifyGraphRenamed(class UEdGraph* Graph, FName OldName, FName NewName) override; +#endif + virtual void Serialize(FArchive& Ar) override; UPackage* GetWidgetTemplatePackage() const; diff --git a/Engine/Source/Editor/UnrealEd/Classes/Commandlets/DiffAssetRegistriesCommandlet.h b/Engine/Source/Editor/UnrealEd/Classes/Commandlets/DiffAssetRegistriesCommandlet.h index 927847371bcd..8915edf2f445 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Commandlets/DiffAssetRegistriesCommandlet.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Commandlets/DiffAssetRegistriesCommandlet.h @@ -164,6 +164,12 @@ private: // Warn when any class of assets has changed by this amount (0=disabled) int32 WarnPercentage; + // Don't warn any class of assets with a total change size lower than this amount (0=disabled) + int32 WarnSizeMinMB; + + // Warn when the total changes are greater than this amount (0=disabled) + int32 WarnTotalChangedSizeMB; + // Platform we're working on, only used for reporting clarity FString TargetPlatform; diff --git a/Engine/Source/Editor/UnrealEd/Classes/Commandlets/ParticleSystemAuditCommandlet.h b/Engine/Source/Editor/UnrealEd/Classes/Commandlets/ParticleSystemAuditCommandlet.h index 237c40adc4d6..fa0e8c305305 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Commandlets/ParticleSystemAuditCommandlet.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Commandlets/ParticleSystemAuditCommandlet.h @@ -30,6 +30,8 @@ class UParticleSystemAuditCommandlet : public UCommandlet TSet ParticleSystemsWithHighSpawnRateOrBurst; /** All particle systems w/ a far LODDistance */ TSet ParticleSystemsWithFarLODDistance; + /** All particle systems w/ bone location sources that do not match between LODs */ + TSet ParticleSystemsWithBoneLocationMismatches; /** If a particle system has a spawn rate or burst count greater than this value, it will be reported */ UPROPERTY(config) diff --git a/Engine/Source/Editor/UnrealEd/Classes/Editor/EditorEngine.h b/Engine/Source/Editor/UnrealEd/Classes/Editor/EditorEngine.h index bc806ed1c1ca..f18a05d7bce3 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Editor/EditorEngine.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Editor/EditorEngine.h @@ -1833,6 +1833,11 @@ public: */ TSharedRef GetTimerManager() { return TimerManager.ToSharedRef(); } + /** + * Returns true if the editors timer manager is valid (may not be during early startup); + */ + bool IsTimerManagerValid() { return TimerManager.IsValid(); } + /** * Returns the Editors world manager instance. */ diff --git a/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxImportUI.h b/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxImportUI.h index 46fcf924a3d5..b3e66f092539 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxImportUI.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxImportUI.h @@ -68,6 +68,16 @@ namespace ImportCompareHelper } }; + enum class ECompareResult : int32 + { + SCR_None = 0x00000000, + SCR_SkeletonMissingBone = 0x00000001, + SCR_SkeletonAddedBone = 0x00000002, + SCR_SkeletonBadRoot = 0x00000004, + }; + + ENUM_CLASS_FLAGS(ECompareResult); + struct FSkeletonCompareData { FSkeletonTreeNode CurrentAssetRoot; @@ -76,10 +86,10 @@ namespace ImportCompareHelper { CurrentAssetRoot.Empty(); ResultAssetRoot.Empty(); - bHasConflict = false; + CompareResult = ECompareResult::SCR_None; } - bool HasConflict() { return bHasConflict; } - bool bHasConflict; + ECompareResult GetCompareResult() { return CompareResult; } + ECompareResult CompareResult; }; } diff --git a/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxSkeletalMeshImportData.h b/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxSkeletalMeshImportData.h index e519f7a136a4..a379d13efd57 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxSkeletalMeshImportData.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Factories/FbxSkeletalMeshImportData.h @@ -21,6 +21,26 @@ enum EFBXImportContentType FBXICT_MAX, }; +namespace NSSkeletalMeshSourceFileLabels +{ + static FText GeoAndSkinningText() + { + static FText GeoAndSkinningText = (NSLOCTEXT("FBXReimport", "ImportContentTypeAll", "Geometry and Skinning Weights")); + return GeoAndSkinningText; + } + + static FText GeometryText() + { + static FText GeometryText = (NSLOCTEXT("FBXReimport", "ImportContentTypeGeometry", "Geometry")); + return GeometryText; + } + static FText SkinningText() + { + static FText SkinningText = (NSLOCTEXT("FBXReimport", "ImportContentTypeSkinning", "Skinning Weights")); + return SkinningText; + } +} + /** * Import data and options used when importing a static mesh from fbx */ @@ -30,8 +50,12 @@ class UFbxSkeletalMeshImportData : public UFbxMeshImportData GENERATED_UCLASS_BODY() public: /** Filter the content we want to import from the incoming FBX skeletal mesh.*/ - UPROPERTY(EditAnywhere, Transient, Category = Mesh, meta = (ImportType = "SkeletalMesh", DisplayName = "Import Content Type", OBJRestrict = "true")) + UPROPERTY(EditAnywhere, Category = Mesh, meta = (ImportType = "SkeletalMesh", DisplayName = "Import Content Type", OBJRestrict = "true")) TEnumAsByte ImportContentType; + + /** The value of the content type during the last import. This cannot be edited and is set only on successful import or re-import*/ + UPROPERTY() + TEnumAsByte LastImportContentType; /** Specify how vertex colors should be imported */ UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, config, Category = Mesh, meta = (OBJRestrict = "true", ImportType = "SkeletalMesh")) @@ -77,6 +101,11 @@ public: static UFbxSkeletalMeshImportData* GetImportDataForSkeletalMesh(USkeletalMesh* SkeletalMesh, UFbxSkeletalMeshImportData* TemplateForCreation); bool CanEditChange( const UProperty* InProperty ) const override; + + bool GetImportContentFilename(FString& OutFilename, FString& OutFilenameLabel) const; + + /** This function add the last import content type to the asset registry which is use by the thumbnail overlay of the skeletal mesh */ + virtual void AppendAssetRegistryTags(TArray& OutTags); }; class FSkeletalMeshImportData; diff --git a/Engine/Source/Editor/UnrealEd/Classes/Factories/ReimportFbxSkeletalMeshFactory.h b/Engine/Source/Editor/UnrealEd/Classes/Factories/ReimportFbxSkeletalMeshFactory.h index 12ae7c3adc83..05fd1675b93a 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Factories/ReimportFbxSkeletalMeshFactory.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Factories/ReimportFbxSkeletalMeshFactory.h @@ -20,8 +20,16 @@ class UReimportFbxSkeletalMeshFactory : public UFbxFactory, public FReimportHand //~ Begin FReimportHandler Interface virtual bool CanReimport( UObject* Obj, TArray& OutFilenames ) override; - virtual void SetReimportPaths( UObject* Obj, const TArray& NewReimportPaths ) override; - virtual EReimportResult::Type Reimport( UObject* Obj ) override; + virtual void SetReimportPaths( UObject* Obj, const TArray& NewReimportPaths ) override + { + return SetReimportPaths(Obj, NewReimportPaths[0], 0); + } + virtual void SetReimportPaths(UObject* Obj, const FString& NewReimportPath, const int32 SourceIndex) override; + virtual EReimportResult::Type Reimport(UObject* Obj) override + { + return Reimport(Obj, INDEX_NONE); + } + virtual EReimportResult::Type Reimport( UObject* Obj, int32 SourceFileIndex ); virtual int32 GetPriority() const override; //~ End FReimportHandler Interface diff --git a/Engine/Source/Editor/UnrealEd/Classes/Settings/EditorExperimentalSettings.h b/Engine/Source/Editor/UnrealEd/Classes/Settings/EditorExperimentalSettings.h index 85cd59e3b24c..39972b766291 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Settings/EditorExperimentalSettings.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Settings/EditorExperimentalSettings.h @@ -24,6 +24,10 @@ public: UPROPERTY(EditAnywhere, config, Category = Foliage, meta = (DisplayName = "Procedural Foliage")) bool bProceduralFoliage; + /** Allows usage of the procedural landscape system*/ + UPROPERTY(EditAnywhere, config, Category = Landscape, meta = (DisplayName = "Procedural Landscape")) + bool bProceduralLandscape; + /** Allows usage of the Localization Dashboard */ UPROPERTY(EditAnywhere, config, Category = Tools, meta = (DisplayName = "Localization Dashboard")) bool bEnableLocalizationDashboard; diff --git a/Engine/Source/Editor/UnrealEd/Classes/Settings/ProjectPackagingSettings.h b/Engine/Source/Editor/UnrealEd/Classes/Settings/ProjectPackagingSettings.h index 7e30c1b2e80d..8b8ae7129591 100644 --- a/Engine/Source/Editor/UnrealEd/Classes/Settings/ProjectPackagingSettings.h +++ b/Engine/Source/Editor/UnrealEd/Classes/Settings/ProjectPackagingSettings.h @@ -167,12 +167,26 @@ public: bool bGenerateNoChunks; /** - * Normally during chunk generation all dependencies of a package in a chunk will be pulled into that package's chunk. - * If this is enabled then only hard dependencies are pulled in. Soft dependencies stay in their original chunk. - */ + * Normally during chunk generation all dependencies of a package in a chunk will be pulled into that package's chunk. + * If this is enabled then only hard dependencies are pulled in. Soft dependencies stay in their original chunk. + */ UPROPERTY(config, EditAnywhere, Category = Packaging) bool bChunkHardReferencesOnly; + /** + * If true, individual files are only allowed to be in a single chunk and it will assign it to the lowest number requested + * If false, it may end up in multiple chunks if requested by the cooker + */ + UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay) + bool bForceOneChunkPerFile; + + /** + * If > 0 this sets a maximum size per chunk. Chunks larger than this size will be split into multiple pak files such as pakchunk0_s1 + * This can be set in platform specific game.ini files + */ + UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay) + int64 MaxChunkSize; + /** * If enabled, will generate data for HTTP Chunk Installer. This data can be hosted on webserver to be installed at runtime. Requires "Generate Chunks" enabled. */ @@ -266,19 +280,55 @@ public: */ UPROPERTY(config) bool bEncryptPakIndex_DEPRECATED; + + /** + * Enable the early downloader pak file pakearly.txt + * This has been superseded by the functionality in DefaultPakFileRules.ini + */ + UPROPERTY(config) + bool GenerateEarlyDownloaderPakFile_DEPRECATED; /** - * Don't include content in any editor folders when cooking. This can cause issues with missing content in cooked games if the content is being used. - */ + * Don't include content in any editor folders when cooking. This can cause issues with missing content in cooked games if the content is being used. + */ UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay, meta = (DisplayName = "Exclude editor content when cooking")) bool bSkipEditorContent; /** - * Don't include movies when staging/packaging + * Don't include movies by default when staging/packaging + * Specific movies can be specified below, and this can be in a platform ini */ UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay, meta = (DisplayName = "Exclude movie files when staging")) bool bSkipMovies; + /** + * If SkipMovies is true, these specific movies will still be added to the .pak file (if using a .pak file; otherwise they're copied as individual files) + * This should be the name with no extension + */ + UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay, meta = (DisplayName = "Specific movies to Package")) + TArray UFSMovies; + + /** + * If SkipMovies is true, these specific movies will be copied when packaging your project, but are not supposed to be part of the .pak file + * This should be the name with no extension + */ + UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay, meta = (DisplayName = "Specific movies to Copy")) + TArray NonUFSMovies; + + /** + * If set, only these specific pak files will be compressed. This should take the form of "*pakchunk0*" + * This can be set in a platform-specific ini file + */ + UPROPERTY(config, EditAnywhere, Category = Packaging, AdvancedDisplay) + TArray CompressedChunkWildcard; + + /** + * List of specific files to include with GenerateEarlyDownloaderPakFile + */ + UPROPERTY(config) + TArray EarlyDownloaderPakFileFiles_DEPRECATED; + + /** * List of maps to include when no other map list is specified on commandline */ diff --git a/Engine/Source/Editor/UnrealEd/Private/Commandlets/AssetRegistryGenerator.cpp b/Engine/Source/Editor/UnrealEd/Private/Commandlets/AssetRegistryGenerator.cpp index d2bd8f99de38..4697163bd74e 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Commandlets/AssetRegistryGenerator.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Commandlets/AssetRegistryGenerator.cpp @@ -425,7 +425,7 @@ void FAssetRegistryGenerator::InjectEncryptionData(FAssetRegistryState& TargetSt TMap GuidCache; TEncryptedAssetSet EncryptedAssetSet; - TSet ReleasedAssets; + TSet ReleasedAssets; AssetManager.GetEncryptedAssetSet(EncryptedAssetSet, ReleasedAssets); for (TEncryptedAssetSet::ElementType EncryptedAssetSetElement : EncryptedAssetSet) diff --git a/Engine/Source/Editor/UnrealEd/Private/Commandlets/DiffAssetRegistriesCommandlet.cpp b/Engine/Source/Editor/UnrealEd/Private/Commandlets/DiffAssetRegistriesCommandlet.cpp index f096960dd763..637685028ef3 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Commandlets/DiffAssetRegistriesCommandlet.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Commandlets/DiffAssetRegistriesCommandlet.cpp @@ -125,6 +125,8 @@ int32 UDiffAssetRegistriesCommandlet::Main(const FString& FullCommandLine) FParse::Value(*FullCommandLine, TEXT("MinChangeSize="), MinChangeSizeMB); FParse::Value(*FullCommandLine, TEXT("ChunkID="), DiffChunkID); FParse::Value(*FullCommandLine, TEXT("WarnPercentage="), WarnPercentage); + FParse::Value(*FullCommandLine, TEXT("WarnSizeMin="), WarnSizeMinMB); + FParse::Value(*FullCommandLine, TEXT("WarnTotalChangedSize="), WarnTotalChangedSizeMB); FString OldPath; FString NewPath; @@ -1190,7 +1192,8 @@ void UDiffAssetRegistriesCommandlet::DiffAssetRegistries(const FString& OldPath, // Warn on a certain % of changes if that's enabled if (Changes.Changes >= 10 - && WarnPercentage > 0 + && (WarnPercentage > 0 || WarnSizeMinMB > 0) + && Changes.ChangedBytes * InvToMB >= WarnSizeMinMB && Changes.GetChangePercentage() * 100.0 > WarnPercentage) { UE_LOG(LogDiffAssets, Warning, TEXT("\t%s Assets for %s are %.02f%% changed. (%.02f MB of data)"), @@ -1259,6 +1262,13 @@ void UDiffAssetRegistriesCommandlet::DiffAssetRegistries(const FString& OldPath, UE_LOG(LogDiffAssets, Display, TEXT("direct %d total packages modified, %8.3f MB"), NondeterministicSummary.Changes, NondeterministicSummary.ChangedBytes * InvToMB); UE_LOG(LogDiffAssets, Display, TEXT("indirect %d total packages modified, %8.3f MB"), IndirectNondeterministicSummary.Changes, IndirectNondeterministicSummary.ChangedBytes * InvToMB); + //Warn when meeting or exceeding a certain total changed size, if that's enabled + if (WarnTotalChangedSizeMB > 0 + && ChangeSummary.ChangedBytes * InvToMB >= WarnTotalChangedSizeMB) + { + UE_LOG(LogDiffAssets, Warning, TEXT("Total Changed Bytes exceeded %d MB! (%8.3f MB)"), WarnTotalChangedSizeMB, ChangeSummary.ChangedBytes * InvToMB); + } + if (CSVFile) { CSVFile->Logf(TEXT("")); diff --git a/Engine/Source/Editor/UnrealEd/Private/Commandlets/GenerateTextLocalizationResourceCommandlet.cpp b/Engine/Source/Editor/UnrealEd/Private/Commandlets/GenerateTextLocalizationResourceCommandlet.cpp index 2ba0f8e3f33e..a5eeb6687c49 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Commandlets/GenerateTextLocalizationResourceCommandlet.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Commandlets/GenerateTextLocalizationResourceCommandlet.cpp @@ -160,7 +160,7 @@ int32 UGenerateTextLocalizationResourceCommandlet::Main(const FString& Params) const bool bLocResFileSaved = FLocalizedAssetSCCUtil::SaveFileWithSCC(SourceControlInfo, TextLocalizationResourcePath, [&LocTextHelper, &CultureName, &bSkipSourceCheck](const FString& InSaveFileName) -> bool { FTextLocalizationResource LocRes; - return FTextLocalizationResourceGenerator::GenerateLocRes(LocTextHelper, CultureName, bSkipSourceCheck, FTextLocalizationResourceId(InSaveFileName), LocRes) && LocRes.SaveToFile(InSaveFileName); + return FTextLocalizationResourceGenerator::GenerateLocRes(LocTextHelper, CultureName, bSkipSourceCheck, FTextKey(InSaveFileName), LocRes) && LocRes.SaveToFile(InSaveFileName); }); if (!bLocResFileSaved) diff --git a/Engine/Source/Editor/UnrealEd/Private/Commandlets/PackageUtilities.cpp b/Engine/Source/Editor/UnrealEd/Private/Commandlets/PackageUtilities.cpp index 1d5bc9a31354..5845dfb81b19 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Commandlets/PackageUtilities.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Commandlets/PackageUtilities.cpp @@ -1607,6 +1607,7 @@ int32 UPkgInfoCommandlet::Main( const FString& Params ) { Reporter->GeneratePackageReport(Linker, *OutputOverride); } +#if !NO_LOGGING if (bDumpProperties) { check(Reader); @@ -1635,6 +1636,7 @@ int32 UPkgInfoCommandlet::Main( const FString& Params ) } Out.Logf(ELogVerbosity::Display, TEXT("Total number of Serialize calls: %lld"), TotalSerializeCalls); } +#endif // !NO_LOGGING } CollectGarbage(RF_NoFlags); } diff --git a/Engine/Source/Editor/UnrealEd/Private/Commandlets/ParticleSystemAuditCommandlet.cpp b/Engine/Source/Editor/UnrealEd/Private/Commandlets/ParticleSystemAuditCommandlet.cpp index 0a883023a7cb..603377ba0d71 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Commandlets/ParticleSystemAuditCommandlet.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Commandlets/ParticleSystemAuditCommandlet.cpp @@ -20,6 +20,7 @@ #include "ICollectionManager.h" #include "CollectionManagerModule.h" #include "Particles/ParticleLODLevel.h" +#include "Particles/Location/ParticleModuleLocationBoneSocket.h" DEFINE_LOG_CATEGORY_STATIC(LogParticleSystemAuditCommandlet, Log, All); @@ -87,7 +88,7 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() const FString PSysName = AssetIt.ObjectPath.ToString(); const FString PackageName = AssetIt.PackageName.ToString(); - if ( PackageName.StartsWith(DevelopersFolder) ) + if (PackageName.StartsWith(DevelopersFolder)) { // Skip developer folders continue; @@ -121,6 +122,7 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() bool bHasRibbonTrailOrBeam = false; bool bHasOnlyBeamsOrHasNoEmitters = true; bool bHasSpawnPerUnit = false; + bool bMismatchedLODBoneModules = false; for (int32 EmitterIdx = 0; EmitterIdx < PSys->Emitters.Num(); EmitterIdx++) { UParticleEmitter* Emitter = PSys->Emitters[EmitterIdx]; @@ -135,6 +137,7 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() bSingleLOD = true; } bFoundEmitter = true; + int32 BoneLocationArraySize = -1; for (int32 LODIdx = 0; LODIdx < Emitter->LODLevels.Num(); LODIdx++) { UParticleLODLevel* LODLevel = Emitter->LODLevels[LODIdx]; @@ -166,19 +169,19 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() if (UParticleModuleSpawn* SpawnModule = Cast(Module)) { - if ( !bHasHighSpawnRateOrBurst ) + if (!bHasHighSpawnRateOrBurst) { - if ( UDistributionFloatConstant* ConstantDistribution = Cast(SpawnModule->Rate.Distribution) ) + if (UDistributionFloatConstant* ConstantDistribution = Cast(SpawnModule->Rate.Distribution)) { - if ( ConstantDistribution->Constant > HighSpawnRateOrBurstThreshold ) + if (ConstantDistribution->Constant > HighSpawnRateOrBurstThreshold) { bHasHighSpawnRateOrBurst = true; } } - for ( const FParticleBurst& Burst : SpawnModule->BurstList ) + for (const FParticleBurst& Burst : SpawnModule->BurstList) { - if ( Burst.Count > HighSpawnRateOrBurstThreshold ) + if (Burst.Count > HighSpawnRateOrBurstThreshold) { bHasHighSpawnRateOrBurst = true; } @@ -189,6 +192,20 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() { bHasSpawnPerUnit = true; } + else if (UParticleModuleLocationBoneSocket* BoneModule = Cast(Module)) + { + int32 SourceArraySize = BoneModule->SourceLocations.Num(); + if (BoneLocationArraySize >= 0 && BoneLocationArraySize != SourceArraySize) + { + // we assume that there is not more than one bone location module per emitter, + // so the mismatch has to come from the LOD levels + bMismatchedLODBoneModules = true; + } + else + { + BoneLocationArraySize = SourceArraySize; + } + } } } } @@ -196,14 +213,14 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() } // Note all PSystems w/ a high constant spawn rate or burst count... - if ( bHasHighSpawnRateOrBurst ) + if (bHasHighSpawnRateOrBurst) { ParticleSystemsWithHighSpawnRateOrBurst.Add(PSys->GetPathName()); } // Note all PSystems w/ a far LOD distance... bool bAtLeastOneLODUnderFarDistanceThresholdOrEmpty = (PSys->LODDistances.Num() == 0); - for ( float LODDistance : PSys->LODDistances ) + for (float LODDistance : PSys->LODDistances) { if (LODDistance <= FarLODDistanceTheshold) { @@ -252,6 +269,12 @@ bool UParticleSystemAuditCommandlet::ProcessParticleSystems() ParticleSystemsWithOrientZAxisTowardCamera.Add(PSys->GetPathName()); } + // Note all systems with problematic bone location source arrays + if (bMismatchedLODBoneModules) + { + ParticleSystemsWithBoneLocationMismatches.Add(PSys->GetPathName()); + } + if ((PSys->LODMethod == PARTICLESYSTEMLODMETHOD_Automatic) && (bInvalidLOD == false) && (bSingleLOD == false) && (PSys->LODDistanceCheckTime == 0.0f)) @@ -308,6 +331,7 @@ void UParticleSystemAuditCommandlet::DumpResults() DumpSimplePSysSet(ParticleSystemsWithOrientZAxisTowardCamera, TEXT("PSysOrientZTowardsCamera")); DumpSimplePSysSet(ParticleSystemsWithHighSpawnRateOrBurst, TEXT("PSysHighSpawnRateOrBurst")); DumpSimplePSysSet(ParticleSystemsWithFarLODDistance, TEXT("PSysFarLODDistance")); + DumpSimplePSysSet(ParticleSystemsWithBoneLocationMismatches, TEXT("PSysBoneLocationLODMismatches")); } /** @@ -361,4 +385,4 @@ FArchive* UParticleSystemAuditCommandlet::GetOutputFile(const TCHAR* InShortFile UE_LOG(LogParticleSystemAuditCommandlet, Warning, TEXT("Failed to create output stream %s"), *Filename); } return OutputStream; -} +} \ No newline at end of file diff --git a/Engine/Source/Editor/UnrealEd/Private/CookOnTheFlyServer.cpp b/Engine/Source/Editor/UnrealEd/Private/CookOnTheFlyServer.cpp index 2bae3c98f6fb..d95a8f55b26a 100644 --- a/Engine/Source/Editor/UnrealEd/Private/CookOnTheFlyServer.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/CookOnTheFlyServer.cpp @@ -6093,7 +6093,9 @@ void UCookOnTheFlyServer::ProcessShaderCodeLibraries(const FString& LibraryName) FString Args(TEXT("build ")); + Args += TEXT("\""); Args += StablePCPath; + Args += TEXT("\""); int32 NumMatched = 0; for (int32 Index = 0; Index < SCLCSVPaths->Num(); Index++) @@ -6104,7 +6106,9 @@ void UCookOnTheFlyServer::ProcessShaderCodeLibraries(const FString& LibraryName) } NumMatched++; Args += TEXT(" "); + Args += TEXT("\""); Args += (*SCLCSVPaths)[Index]; + Args += TEXT("\""); } if (!NumMatched) { @@ -6117,7 +6121,9 @@ void UCookOnTheFlyServer::ProcessShaderCodeLibraries(const FString& LibraryName) } Args += TEXT(" "); + Args += TEXT("\""); Args += PCPath; + Args += TEXT("\""); UE_LOG(LogCook, Display, TEXT(" With Args: %s"), *Args); int32 Result = UShaderPipelineCacheToolsCommandlet::StaticMain(Args); diff --git a/Engine/Source/Editor/UnrealEd/Private/Editor.cpp b/Engine/Source/Editor/UnrealEd/Private/Editor.cpp index 4be810a4990d..daf4a74eaad7 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Editor.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Editor.cpp @@ -65,6 +65,7 @@ #include "K2Node_AddComponent.h" #include "AutoReimport/AutoReimportUtilities.h" +#include "AssetToolsModule.h" #define LOCTEXT_NAMESPACE "UnrealEd.Editor" @@ -134,7 +135,10 @@ FEditorDelegates::FOnDuplicateActorsBegin FEditorDelegates::OnDuplicateActors FEditorDelegates::FOnDuplicateActorsEnd FEditorDelegates::OnDuplicateActorsEnd; FEditorDelegates::FOnDeleteActorsBegin FEditorDelegates::OnDeleteActorsBegin; FEditorDelegates::FOnDeleteActorsEnd FEditorDelegates::OnDeleteActorsEnd; - +FEditorDelegates::FOnViewAssetIdentifiers FEditorDelegates::OnOpenReferenceViewer; +FEditorDelegates::FOnViewAssetIdentifiers FEditorDelegates::OnOpenSizeMap; +FEditorDelegates::FOnViewAssetIdentifiers FEditorDelegates::OnOpenAssetAudit; +FEditorDelegates::FOnViewAssetIdentifiers FEditorDelegates::OnEditAssetIdentifiers; /*----------------------------------------------------------------------------- Globals. @@ -204,7 +208,31 @@ void FReimportManager::UpdateReimportPaths( UObject* Obj, const TArray& } } -bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, bool bShowNotification, FString PreferredReimportFile, FReimportHandler* SpecifiedReimportHandler) +void FReimportManager::UpdateReimportPath(UObject* Obj, const FString& Filename, int32 SourceFileIndex) +{ + if (Obj) + { + TArray UnusedExistingFilenames; + auto* Handler = Handlers.FindByPredicate([&](FReimportHandler* InHandler) { return InHandler->CanReimport(Obj, UnusedExistingFilenames); }); + if (Handler) + { + if (SourceFileIndex == INDEX_NONE) + { + TArray Filenames; + Filenames.Add(Filename); + (*Handler)->SetReimportPaths(Obj, Filenames); + } + else + { + (*Handler)->SetReimportPaths(Obj, Filename, SourceFileIndex); + } + Obj->MarkPackageDirty(); + } + } +} + + +bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, bool bShowNotification, FString PreferredReimportFile, FReimportHandler* SpecifiedReimportHandler, int32 SourceFileIndex, bool bForceNewFile /*= false*/) { // Warn that were about to reimport, so prep for it PreReimport.Broadcast( Obj ); @@ -238,21 +266,26 @@ bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, boo if(CanReimportHandler != nullptr) { + TArray MissingFileIndex; // Check all filenames for missing files bool bMissingFiles = false; if (SourceFilenames.Num() > 0) { for (int32 FileIndex = 0; FileIndex < SourceFilenames.Num(); ++FileIndex) { - if (SourceFilenames[FileIndex].IsEmpty() || IFileManager::Get().FileSize(*SourceFilenames[FileIndex]) == INDEX_NONE) + if (SourceFilenames[FileIndex].IsEmpty() || IFileManager::Get().FileSize(*SourceFilenames[FileIndex]) == INDEX_NONE || (bForceNewFile && SourceFileIndex == FileIndex)) { - bMissingFiles = true; - break; + if (SourceFileIndex == INDEX_NONE || SourceFileIndex == FileIndex) + { + MissingFileIndex.AddUnique(FileIndex); + bMissingFiles = true; + } } } } else { + MissingFileIndex.AddUnique(SourceFileIndex == INDEX_NONE ? 0 : SourceFileIndex); bMissingFiles = true; } @@ -266,7 +299,10 @@ bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, boo } else { - GetNewReimportPath(Obj, SourceFilenames); + for (int32 FileIndex : MissingFileIndex) + { + GetNewReimportPath(Obj, SourceFilenames, FileIndex); + } } if ( SourceFilenames.Num() == 0 ) { @@ -277,21 +313,19 @@ bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, boo else { // A new filename was supplied, update the path - CanReimportHandler->SetReimportPaths(Obj, SourceFilenames); + CanReimportHandler->SetReimportPaths(Obj, SourceFilenames[0], SourceFileIndex); } } else if (!PreferredReimportFile.IsEmpty() && !SourceFilenames.Contains(PreferredReimportFile)) { // Reimporting the asset from a new file - SourceFilenames.Empty(); - SourceFilenames.Add(PreferredReimportFile); - CanReimportHandler->SetReimportPaths(Obj, SourceFilenames); + CanReimportHandler->SetReimportPaths(Obj, PreferredReimportFile, SourceFileIndex); } if ( bValidSourceFilename ) { // Do the reimport - EReimportResult::Type Result = CanReimportHandler->Reimport( Obj ); + EReimportResult::Type Result = CanReimportHandler->Reimport( Obj, SourceFileIndex ); if( Result == EReimportResult::Succeeded ) { Obj->PostEditChange(); @@ -374,11 +408,11 @@ bool FReimportManager::Reimport( UObject* Obj, bool bAskForNewFileIfMissing, boo return bSuccess; } -void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImportObjects, bool bShowNotification) +void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImportObjects, bool bShowNotification, int32 SourceFileIndex, bool bForceNewFile /*= false*/) { //Copy the array to prevent iteration assert if a reimport factory change the selection TArray CopyOfSelectedAssets; - TArray MissingFileSelectedAssets; + TMap> MissingFileSelectedAssets; for (UObject *Asset : ToImportObjects) { TArray SourceFilenames; @@ -386,18 +420,20 @@ void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImpo { if (SourceFilenames.Num() == 0) { - MissingFileSelectedAssets.Add(Asset); + MissingFileSelectedAssets.FindOrAdd(Asset); } else { bool bMissingFile = false; - for (FString SourceFilename : SourceFilenames) + + for (int32 FileIndex = 0; FileIndex < SourceFilenames.Num(); ++FileIndex) { - if (SourceFilename.IsEmpty() || IFileManager::Get().FileSize(*SourceFilename) == INDEX_NONE) + FString SourceFilename = SourceFilenames[FileIndex]; + if (SourceFilename.IsEmpty() || IFileManager::Get().FileSize(*SourceFilename) == INDEX_NONE || (bForceNewFile && FileIndex == SourceFileIndex)) { - MissingFileSelectedAssets.Add(Asset); + TArray& SourceIndexArray = MissingFileSelectedAssets.FindOrAdd(Asset); + SourceIndexArray.Add(FileIndex); bMissingFile = true; - break; } } @@ -423,8 +459,10 @@ void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImpo Arguments.Add(TEXT("MissingNumber"), FText::FromString(FString::FromInt(MissingFileSelectedAssets.Num()))); int MaxListFile = 100; FString AssetToFileListString; - for (UObject *Asset : MissingFileSelectedAssets) + for (auto Kvp : MissingFileSelectedAssets) { + UObject *Asset = Kvp.Key; + const TArray& SourceIndexArray = Kvp.Value; AssetToFileListString += TEXT("\n"); if (MaxListFile == 0) { @@ -435,7 +473,15 @@ void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImpo if (this->CanReimport(Asset, &SourceFilenames)) { MaxListFile--; - AssetToFileListString += FString::Printf(TEXT("Asset %s -> Missing file %s"), *(Asset->GetName()), *(SourceFilenames[0])); + for (int32 FileIndex : SourceIndexArray) + { + int32 RemapFileIndex = 0; + if (SourceFilenames.IsValidIndex(FileIndex)) + { + RemapFileIndex = FileIndex; + } + AssetToFileListString += FString::Printf(TEXT("Asset %s -> Missing file %s"), *(Asset->GetName()), *(SourceFilenames[RemapFileIndex])); + } } } Arguments.Add(TEXT("AssetToFileList"), FText::FromString(AssetToFileListString)); @@ -447,16 +493,29 @@ void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImpo //Ask missing file locations if (UserChoice == EAppReturnType::Type::Yes) { + bool bCancelAll = true; //Ask the user for a new source reimport path for each asset - for (UObject *Asset : MissingFileSelectedAssets) + for (auto Kvp : MissingFileSelectedAssets) { - TArray SourceFilenames; - this->GetNewReimportPath(Asset, SourceFilenames); - if (SourceFilenames.Num() == 0) + UObject *Asset = Kvp.Key; + const TArray& SourceIndexArray = Kvp.Value; + for (int32 FileIndex : SourceIndexArray) { - continue; + TArray SourceFilenames; + this->GetNewReimportPath(Asset, SourceFilenames, FileIndex); + if (SourceFilenames.Num() == 0) + { + continue; + } + bCancelAll = false; + this->UpdateReimportPath(Asset, SourceFilenames[0], FileIndex); } - this->UpdateReimportPaths(Asset, SourceFilenames); + //return if the operation is cancel and we have nothing to re-import + if (bCancelAll) + { + return; + } + CopyOfSelectedAssets.Add(Asset); } } @@ -467,7 +526,7 @@ void FReimportManager::ValidateAllSourceFileAndReimport(TArray &ToImpo //If user ignore those asset just not add them to CopyOfSelectedAssets } - FReimportManager::Instance()->ReimportMultiple(CopyOfSelectedAssets, /*bAskForNewFileIfMissing=*/false, bShowNotification); + FReimportManager::Instance()->ReimportMultiple(CopyOfSelectedAssets, /*bAskForNewFileIfMissing=*/false, bShowNotification, TEXT(""), nullptr, SourceFileIndex); } void FReimportManager::AddReferencedObjects( FReferenceCollector& Collector ) @@ -482,7 +541,7 @@ void FReimportManager::AddReferencedObjects( FReferenceCollector& Collector ) } } -bool FReimportManager::ReimportMultiple(TArrayView Objects, bool bAskForNewFileIfMissing /*= false*/, bool bShowNotification /*= true*/, FString PreferredReimportFile /*= TEXT("")*/, FReimportHandler* SpecifiedReimportHandler /*= nullptr */) +bool FReimportManager::ReimportMultiple(TArrayView Objects, bool bAskForNewFileIfMissing /*= false*/, bool bShowNotification /*= true*/, FString PreferredReimportFile /*= TEXT("")*/, FReimportHandler* SpecifiedReimportHandler /*= nullptr */, int32 SourceFileIndex /*= INDEX_NONE*/) { bool bBulkSuccess = true; @@ -496,7 +555,7 @@ bool FReimportManager::ReimportMultiple(TArrayView Objects, bool bAskF FScopedSlowTask SingleObjectTask(1.0f, SingleTaskTest); SingleObjectTask.EnterProgressFrame(1.0f); - bBulkSuccess = bBulkSuccess && Reimport(CurrentObject, bAskForNewFileIfMissing, bShowNotification, PreferredReimportFile, SpecifiedReimportHandler); + bBulkSuccess = bBulkSuccess && Reimport(CurrentObject, bAskForNewFileIfMissing, bShowNotification, PreferredReimportFile, SpecifiedReimportHandler, SourceFileIndex); } BulkReimportTask.EnterProgressFrame(1.0f); @@ -505,12 +564,21 @@ bool FReimportManager::ReimportMultiple(TArrayView Objects, bool bAskF return bBulkSuccess; } -void FReimportManager::GetNewReimportPath(UObject* Obj, TArray& InOutFilenames) +void FReimportManager::GetNewReimportPath(UObject* Obj, TArray& InOutFilenames, int32 SourceFileIndex /*= INDEX_NONE*/) { TArray ReturnObjects; FString FileTypes; FString AllExtensions; TArray Factories; + TArray SourceFileLabels; + FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); + const auto AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(Obj->GetClass()); + if (AssetTypeActions.IsValid()) + { + TArray Objects; + Objects.Add(Obj); + AssetTypeActions.Pin()->GetSourceFileLabels(Objects, SourceFileLabels); + } // Determine whether we will allow multi select and clear old filenames bool bAllowMultiSelect = InOutFilenames.Num() > 1; @@ -569,7 +637,28 @@ void FReimportManager::GetNewReimportPath(UObject* Obj, TArray& InOutFi ParentWindowWindowHandle = MainFrameParentWindow->GetNativeWindow()->GetOSWindowHandle(); } - const FString Title = FString::Printf(TEXT("%s: %s"), *NSLOCTEXT("ReimportManager", "ImportDialogTitle", "Import For").ToString(), *Obj->GetName()); + FString Title = FString::Printf(TEXT("%s: %s"), *NSLOCTEXT("ReimportManager", "ImportDialogTitle", "Import For").ToString(), *Obj->GetName()); + if (SourceFileIndex != INDEX_NONE) + { + if (SourceFileLabels.IsValidIndex(SourceFileIndex)) + { + Title = FString::Printf(TEXT("%s %s %s: %s"), + *NSLOCTEXT("ReimportManager", "ImportDialogTitleLabelPart1", "Select").ToString(), + *SourceFileLabels[SourceFileIndex], + *NSLOCTEXT("ReimportManager", "ImportDialogTitleLabelPart2", "Source File For").ToString(), + *Obj->GetName()); + } + else + { + FString SourceFileIndexStr = FString::FromInt(SourceFileIndex); + Title = FString::Printf(TEXT("%s %s %s: %s"), + *NSLOCTEXT("ReimportManager", "ImportDialogTitlePart1", "Select Source File Index").ToString(), + *SourceFileIndexStr, + *NSLOCTEXT("ReimportManager", "ImportDialogTitlePart2", "for").ToString(), + *Obj->GetName()); + } + } + bOpened = DesktopPlatform->OpenFileDialog( ParentWindowWindowHandle, Title, diff --git a/Engine/Source/Editor/UnrealEd/Private/EditorModeManager.cpp b/Engine/Source/Editor/UnrealEd/Private/EditorModeManager.cpp index 1eede4b514fe..4d7e135ca0cd 100644 --- a/Engine/Source/Editor/UnrealEd/Private/EditorModeManager.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/EditorModeManager.cpp @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "EditorModeManager.h" #include "Engine/Selection.h" @@ -743,6 +743,18 @@ bool FEditorModeTools::CapturedMouseMove( FEditorViewportClient* InViewportClien return bHandled; } +/** Notifies all active modes of all captured mouse movement */ +bool FEditorModeTools::ProcessCapturedMouseMoves( FEditorViewportClient* InViewportClient, FViewport* InViewport, const TArrayView& CapturedMouseMoves ) +{ + bool bHandled = false; + for( int32 ModeIndex = 0; ModeIndex < Modes.Num(); ++ModeIndex ) + { + const TSharedPtr& Mode = Modes[ ModeIndex ]; + bHandled |= Mode->ProcessCapturedMouseMoves( InViewportClient, InViewport, CapturedMouseMoves ); + } + return bHandled; +} + /** Notifies all active modes of keyboard input */ bool FEditorModeTools::InputKey(FEditorViewportClient* InViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) { diff --git a/Engine/Source/Editor/UnrealEd/Private/EditorViewportClient.cpp b/Engine/Source/Editor/UnrealEd/Private/EditorViewportClient.cpp index 749acfa5fcdf..0771c3f338e2 100644 --- a/Engine/Source/Editor/UnrealEd/Private/EditorViewportClient.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/EditorViewportClient.cpp @@ -5128,6 +5128,8 @@ void FEditorViewportClient::CapturedMouseMove( FViewport* InViewport, int32 InMo UpdateRequiredCursorVisibility(); ApplyRequiredCursorVisibility(); + CapturedMouseMoves.Add(FIntPoint(InMouseX, InMouseY)); + // Let the current editor mode know about the mouse movement. if (ModeTools->CapturedMouseMove(this, InViewport, InMouseX, InMouseY)) { @@ -5135,6 +5137,12 @@ void FEditorViewportClient::CapturedMouseMove( FViewport* InViewport, int32 InMo } } +void FEditorViewportClient::ProcessAccumulatedPointerInput(FViewport* InViewport) +{ + ModeTools->ProcessCapturedMouseMoves(this, InViewport, CapturedMouseMoves); + CapturedMouseMoves.Reset(); +} + void FEditorViewportClient::OpenScreenshot( FString SourceFilePath ) { FPlatformProcess::ExploreFolder( *( FPaths::GetPath( SourceFilePath ) ) ); diff --git a/Engine/Source/Editor/UnrealEd/Private/Factories/EditorFactories.cpp b/Engine/Source/Editor/UnrealEd/Private/Factories/EditorFactories.cpp index 2a41168139ad..f7c2f82f77a5 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Factories/EditorFactories.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Factories/EditorFactories.cpp @@ -258,6 +258,10 @@ #include "Misc/App.h" +#include "IDesktopPlatform.h" +#include "DesktopPlatformModule.h" +#include "Interfaces/IMainFrameModule.h" + DEFINE_LOG_CATEGORY(LogEditorFactories); #define LOCTEXT_NAMESPACE "EditorFactories" @@ -266,6 +270,56 @@ DEFINE_LOG_CATEGORY(LogEditorFactories); Shared - used by multiple factories ------------------------------------------------------------------------------*/ +void GetReimportPathFromUser(const FText& TitleLabel, TArray& InOutFilenames) +{ + FString FileTypes; + FString AllExtensions; + // Determine whether we will allow multi select and clear old filenames + bool bAllowMultiSelect = false; + InOutFilenames.Empty(); + + FileTypes = TEXT("FBX Files (*.fbx)|*.fbx"); + + FString DefaultFolder; + FString DefaultFile; + + // Prompt the user for the filenames + TArray OpenFilenames; + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + bool bOpened = false; + if (DesktopPlatform) + { + void* ParentWindowWindowHandle = NULL; + + IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); + const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); + if (MainFrameParentWindow.IsValid() && MainFrameParentWindow->GetNativeWindow().IsValid()) + { + ParentWindowWindowHandle = MainFrameParentWindow->GetNativeWindow()->GetOSWindowHandle(); + } + + const FString Title = FString::Printf(TEXT("%s %s"), *NSLOCTEXT("FBXReimport", "ImportContentTypeDialogTitle", "Add import source file for").ToString(), *TitleLabel.ToString()); + bOpened = DesktopPlatform->OpenFileDialog( + ParentWindowWindowHandle, + Title, + *DefaultFolder, + *DefaultFile, + FileTypes, + bAllowMultiSelect ? EFileDialogFlags::Multiple : EFileDialogFlags::None, + OpenFilenames + ); + } + + if (bOpened) + { + for (int32 FileIndex = 0; FileIndex < OpenFilenames.Num(); ++FileIndex) + { + InOutFilenames.Add(OpenFilenames[FileIndex]); + } + } +} + + class FAssetClassParentFilter : public IClassViewerFilter { public: @@ -5266,6 +5320,7 @@ void UReimportFbxStaticMeshFactory::SetReimportPaths( UObject* Obj, const TArray UStaticMesh* Mesh = Cast(Obj); if(Mesh && ensure(NewReimportPaths.Num() == 1)) { + Mesh->Modify(); UFbxStaticMeshImportData* ImportData = UFbxStaticMeshImportData::GetImportDataForStaticMesh(Mesh, ImportUI->StaticMeshImportData); ImportData->UpdateFilenameOnly(NewReimportPaths[0]); @@ -5373,6 +5428,8 @@ EReimportResult::Type UReimportFbxStaticMeshFactory::Reimport( UObject* Obj ) ImportOptions->bAutoComputeLodDistances = true; ImportOptions->LodNumber = 0; ImportOptions->MinimumLodNumber = 0; + //Make sure the LODGroup do not change when re-importing a mesh + ImportOptions->StaticMeshLODGroup = Mesh->LODGroup; if( !bOperationCanceled && ensure(ImportData) ) { @@ -5542,17 +5599,18 @@ bool UReimportFbxSkeletalMeshFactory::CanReimport( UObject* Obj, TArray return false; } -void UReimportFbxSkeletalMeshFactory::SetReimportPaths( UObject* Obj, const TArray& NewReimportPaths ) +void UReimportFbxSkeletalMeshFactory::SetReimportPaths( UObject* Obj, const FString& NewReimportPath, const int32 SourceFileIndex ) { USkeletalMesh* SkeletalMesh = Cast(Obj); - if(SkeletalMesh && ensure(NewReimportPaths.Num() == 1)) + if(SkeletalMesh) { + SkeletalMesh->Modify(); UFbxSkeletalMeshImportData* ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, ImportUI->SkeletalMeshImportData); - ImportData->UpdateFilenameOnly(NewReimportPaths[0]); + ImportData->UpdateFilenameOnly(NewReimportPath, SourceFileIndex); } } -EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) +EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj, int32 SourceFileIndex) { // Only handle valid skeletal meshes if( !Obj || !Obj->IsA( USkeletalMesh::StaticClass() )) @@ -5604,31 +5662,142 @@ EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, ImportUI->SkeletalMeshImportData); SkeletalMesh->AssetImportData = ImportData; } + check(ImportData != nullptr); - const FString Filename = ImportData->GetFirstFilename(); - UE_LOG(LogEditorFactories, Log, TEXT("Performing atomic reimport of [%s]"), *Filename); - - // Ensure that the file provided by the path exists - if (IFileManager::Get().FileSize(*Filename) == INDEX_NONE) + auto GetSourceFileName = [](UFbxSkeletalMeshImportData* ImportDataPtr, FString& OutFilename, bool bUnattended)->bool { - UE_LOG(LogEditorFactories, Warning, TEXT("-- cannot reimport: source file cannot be found.")); - return EReimportResult::Failed; - } - CurrentFilename = Filename; + if (ImportDataPtr == nullptr) + { + return false; + } + EFBXImportContentType ContentType = ImportDataPtr->ImportContentType; + TArray AbsoluteFilenames; + ImportDataPtr->ExtractFilenames(AbsoluteFilenames); + + auto InternalGetSourceFileName = [&ImportDataPtr, &AbsoluteFilenames, &bUnattended, &OutFilename](const int32 SourceIndex, const FText& SourceLabel)->bool + { + if (AbsoluteFilenames.Num() > SourceIndex) + { + OutFilename = AbsoluteFilenames[SourceIndex]; + } + else if (!bUnattended) + { + GetReimportPathFromUser(SourceLabel, AbsoluteFilenames); + if (AbsoluteFilenames.Num() < 1) + { + return false; + } + OutFilename = AbsoluteFilenames[0]; + } + //Make sure the source file data is up to date + if (SourceIndex == 0) + { + //When we re-import the All content we just update the + ImportDataPtr->AddFileName(OutFilename, SourceIndex, SourceLabel.ToString()); + } + else + { + //Refresh the absolute filenames + AbsoluteFilenames.Reset(); + ImportDataPtr->ExtractFilenames(AbsoluteFilenames); + //Set both geo and skinning filepath. Reuse existing file path if possible. Use the first filename(geo and skin) if it has to be create. + FString FilenameToAdd = SourceIndex == 1 ? OutFilename : AbsoluteFilenames.Num() > SourceIndex ? AbsoluteFilenames[1] : AbsoluteFilenames[0]; + ImportDataPtr->AddFileName(FilenameToAdd, 1, NSSkeletalMeshSourceFileLabels::GeometryText().ToString()); + FilenameToAdd = SourceIndex == 2 ? OutFilename : AbsoluteFilenames.Num() > SourceIndex ? AbsoluteFilenames[2] : AbsoluteFilenames[0]; + ImportDataPtr->AddFileName(FilenameToAdd, 2, NSSkeletalMeshSourceFileLabels::SkinningText().ToString()); + } + return true; + }; - if( ImportData && !ShowImportDialogAtReimport) + switch (ContentType) + { + case FBXICT_All: + { + if (!InternalGetSourceFileName(0, NSSkeletalMeshSourceFileLabels::GeoAndSkinningText())) + { + return false; + } + } + break; + case FBXICT_Geometry: + { + if (!InternalGetSourceFileName(1, NSSkeletalMeshSourceFileLabels::GeometryText())) + { + return false; + } + } + break; + case FBXICT_SkinningWeights: + { + if (!InternalGetSourceFileName(2, NSSkeletalMeshSourceFileLabels::SkinningText())) + { + return false; + } + } + break; + default: + { + if (!InternalGetSourceFileName(0, NSSkeletalMeshSourceFileLabels::GeoAndSkinningText())) + { + return false; + } + } + } + return IFileManager::Get().FileSize(*OutFilename) != INDEX_NONE; + }; + + FString Filename = ImportData->GetFirstFilename(); + + ReimportUI->SkeletalMeshImportData = ImportData; + const FSkeletalMeshModel* SkeletalMeshModel = SkeletalMesh->GetImportedModel(); + + //Manage the content type from the source file index + ReimportUI->bAllowContentTypeImport = SkeletalMeshModel && SkeletalMeshModel->LODModels.Num() > 0 && !SkeletalMeshModel->LODModels[0].RawSkeletalMeshBulkData.IsEmpty(); + if (!ReimportUI->bAllowContentTypeImport) + { + //No content type allow reimport All (legacy) + ImportData->ImportContentType = EFBXImportContentType::FBXICT_All; + } + else if (SourceFileIndex != INDEX_NONE) + { + //Reimport a specific source file index + TArray SourceFilenames; + ImportData->ExtractFilenames(SourceFilenames); + if (SourceFilenames.IsValidIndex(SourceFileIndex)) + { + ImportData->ImportContentType = SourceFileIndex == 0 ? EFBXImportContentType::FBXICT_All : SourceFileIndex == 1 ? EFBXImportContentType::FBXICT_Geometry : EFBXImportContentType::FBXICT_SkinningWeights; + Filename = SourceFilenames[SourceFileIndex]; + } + } + else + { + //No source index is provided. Reimport the last imported content. + int32 LastSourceFileIndex = ImportData->LastImportContentType == EFBXImportContentType::FBXICT_All ? 0 : ImportData->LastImportContentType == EFBXImportContentType::FBXICT_Geometry ? 1 : 2; + TArray SourceFilenames; + ImportData->ExtractFilenames(SourceFilenames); + if (SourceFilenames.IsValidIndex(LastSourceFileIndex)) + { + ImportData->ImportContentType = ImportData->LastImportContentType; + Filename = SourceFilenames[LastSourceFileIndex]; + } + else + { + ImportData->ImportContentType = EFBXImportContentType::FBXICT_All; + } + } + + + if( !ShowImportDialogAtReimport) { // Import data already exists, apply it to the fbx import options - ReimportUI->SkeletalMeshImportData = ImportData; //Some options not supported with skeletal mesh - ReimportUI->SkeletalMeshImportData->bBakePivotInVertex = false; - ReimportUI->SkeletalMeshImportData->bTransformVertexToAbsolute = true; + ImportData->bBakePivotInVertex = false; + ImportData->bTransformVertexToAbsolute = true; - const FSkeletalMeshModel* SkeletalMeshModel = SkeletalMesh->GetImportedModel(); - ReimportUI->bAllowContentTypeImport = SkeletalMeshModel && SkeletalMeshModel->LODModels.Num() > 0 && !SkeletalMeshModel->LODModels[0].RawSkeletalMeshBulkData.IsEmpty(); - if (!ReimportUI->bAllowContentTypeImport) + if (!GetSourceFileName(ImportData, Filename, true)) { - ReimportUI->SkeletalMeshImportData->ImportContentType = EFBXImportContentType::FBXICT_All; + UE_LOG(LogEditorFactories, Warning, TEXT("-- cannot reimport: source file cannot be found.")); + return EReimportResult::Failed; } ApplyImportUIToImportOptions(ReimportUI, *ImportOptions); @@ -5637,14 +5806,7 @@ EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) { ReimportUI->bIsReimport = true; ReimportUI->ReimportMesh = Obj; - ReimportUI->SkeletalMeshImportData = ImportData; - const FSkeletalMeshModel* SkeletalMeshModel = SkeletalMesh->GetImportedModel(); - ReimportUI->bAllowContentTypeImport = SkeletalMeshModel && SkeletalMeshModel->LODModels.Num() > 0 && !SkeletalMeshModel->LODModels[0].RawSkeletalMeshBulkData.IsEmpty(); - if (!ReimportUI->bAllowContentTypeImport) - { - ReimportUI->SkeletalMeshImportData->ImportContentType = EFBXImportContentType::FBXICT_All; - } bool bImportOperationCanceled = false; bool bShowOptionDialog = true; bool bForceImportType = true; @@ -5657,9 +5819,18 @@ EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) ImportOptions->PhysicsAsset = SkeletalMesh->PhysicsAsset; ImportOptions = GetImportOptions( FFbxImporter, ReimportUI, bShowOptionDialog, bIsAutomated, Obj->GetPathName(), bOperationCanceled, bOutImportAll, bIsObjFormat, Filename, bForceImportType, FBXIT_SkeletalMesh); + + if (!GetSourceFileName(ImportData, Filename, false)) + { + UE_LOG(LogEditorFactories, Warning, TEXT("-- cannot reimport: source file cannot be found.")); + return EReimportResult::Failed; + } } - if( !bOperationCanceled && ensure(ImportData) ) + UE_LOG(LogEditorFactories, Log, TEXT("Performing atomic reimport of [%s]"), *Filename); + CurrentFilename = Filename; + + if( !bOperationCanceled ) { ImportOptions->bCanShowDialog = !IsUnattended; @@ -5676,7 +5847,6 @@ EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) { ImportOptions->bImportAnimations = false; ImportOptions->bUpdateSkeletonReferencePose = false; - ImportOptions->bUseT0AsRefPose = false; } if ( FFbxImporter->ImportFromFile( *Filename, FPaths::GetExtension( Filename ), true ) ) @@ -5685,8 +5855,6 @@ EReimportResult::Type UReimportFbxSkeletalMeshFactory::Reimport( UObject* Obj ) { UE_LOG(LogEditorFactories, Log, TEXT("-- imported successfully") ); - SkeletalMesh->AssetImportData->Update(Filename); - // Try to find the outer package so we can dirty it up if (SkeletalMesh->GetOuter()) { diff --git a/Engine/Source/Editor/UnrealEd/Private/Factories/SkeletalMeshImport.cpp b/Engine/Source/Editor/UnrealEd/Private/Factories/SkeletalMeshImport.cpp index 7fc184f32cdd..0474b8e08a21 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Factories/SkeletalMeshImport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Factories/SkeletalMeshImport.cpp @@ -535,7 +535,10 @@ ExistingSkelMeshData* SaveExistingSkelMeshData(USkeletalMesh* ExistingSkelMesh, void TryRegenerateLODs(ExistingSkelMeshData* MeshData, USkeletalMesh* SkeletalMesh) { + check(SkeletalMesh != nullptr); int32 TotalLOD = MeshData->ExistingLODModels.Num(); + FSkeletalMeshModel* SkeletalMeshImportedModel = SkeletalMesh->GetImportedModel(); + // see if mesh reduction util is available IMeshReductionManagerModule& Module = FModuleManager::Get().LoadModuleChecked("MeshReductionInterface"); static bool bAutoMeshReductionAvailable = Module.GetSkeletalMeshReductionInterface() != NULL; @@ -554,8 +557,11 @@ void TryRegenerateLODs(ExistingSkelMeshData* MeshData, USkeletalMesh* SkeletalMe if (LODIndex >= SkeletalMesh->GetLODInfoArray().Num()) { FSkeletalMeshLODInfo& ExistLODInfo = MeshData->ExistingLODInfo[Index]; + FSkeletalMeshLODModel& ExistLODModel = MeshData->ExistingLODModels[Index]; // reset material maps, it won't work anyway. ExistLODInfo.LODMaterialMap.Empty(); + + FSkeletalMeshLODModel* NewLODModel = new(SkeletalMeshImportedModel->LODModels) FSkeletalMeshLODModel(ExistLODModel); // add LOD info back SkeletalMesh->AddLODInfo(ExistLODInfo); check(LODIndex < SkeletalMesh->GetLODInfoArray().Num()); @@ -849,20 +855,31 @@ void RestoreExistingSkelMeshData(ExistingSkelMeshData* MeshData, USkeletalMesh* auto ApplySkinning = [&SkeletalMesh, &SkeletalMeshImportedModel, &MeshData, &ApplySkinnings, &RestoreReductionSourceData]() { FSkeletalMeshLODModel& BaseLodModel = SkeletalMeshImportedModel->LODModels[0]; + int32 OffsetLOD = SkeletalMesh->GetLODNum(); //Apply the new skinning on all existing LOD for (int32 Index = 0; Index < MeshData->ExistingLODModels.Num(); ++Index) { + int32 RealIndex = OffsetLOD + Index; if (!ApplySkinnings[Index]) { continue; } FSkeletalMeshLODModel& LODModel = MeshData->ExistingLODModels[Index]; FSkeletalMeshLODInfo& LODInfo = MeshData->ExistingLODInfo[Index]; - FSkeletalMeshLODModel* NewLODModel = new(SkeletalMeshImportedModel->LODModels) FSkeletalMeshLODModel(LODModel); - // add LOD info back - SkeletalMesh->AddLODInfo(LODInfo); - RestoreReductionSourceData(Index, SkeletalMesh->GetLODNum() - 1); + FSkeletalMeshLODModel* NewLODModel = nullptr; + if (RealIndex >= SkeletalMesh->GetLODNum()) + { + NewLODModel = new(SkeletalMeshImportedModel->LODModels) FSkeletalMeshLODModel(LODModel); + // add LOD info back + SkeletalMesh->AddLODInfo(LODInfo); + } + else + { + NewLODModel = &(SkeletalMeshImportedModel->LODModels[RealIndex]); + } + + RestoreReductionSourceData(Index, RealIndex); //Apply the new skinning to the existing LOD geometry SkeletalMeshHelper::ApplySkinning(SkeletalMesh, BaseLodModel, *NewLODModel); @@ -981,6 +998,13 @@ void RestoreExistingSkelMeshData(ExistingSkelMeshData* MeshData, USkeletalMesh* if (bAutoMeshReductionAvailable && LODInfo.bHasBeenSimplified && LODInfo.ReductionSettings.BaseLOD == 0) { bRegenLODs = !bImportSkinningOnly; + if (bRegenLODs) + { + //We need to add LODInfo + FSkeletalMeshLODModel* NewLODModel = new(SkeletalMeshImportedModel->LODModels) FSkeletalMeshLODModel(LODModel); + SkeletalMesh->AddLODInfo(LODInfo); + RestoreReductionSourceData(i, SkeletalMesh->GetLODNum() - 1); + } } else { @@ -1005,12 +1029,9 @@ void RestoreExistingSkelMeshData(ExistingSkelMeshData* MeshData, USkeletalMesh* { TryRegenerateLODs(MeshData, SkeletalMesh); } - else + else if (!bSkinningIsApply) { - if (!bSkinningIsApply) - { - ApplySkinning(); - } + ApplySkinning(); } } diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxAnimationExport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxAnimationExport.cpp index b8099ac144aa..da056ba16224 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxAnimationExport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxAnimationExport.cpp @@ -259,20 +259,56 @@ FbxNode* FFbxExporter::ExportAnimSequence( const UAnimSequence* AnimSeq, const U SkelMesh->GetName(MeshNodeName); } - // Add the mesh - FbxNode* MeshRootNode = CreateMesh(SkelMesh, *MeshNodeName); - if(MeshRootNode) + FbxNode* MeshRootNode = nullptr; + if (GetExportOptions()->LevelOfDetail && SkelMesh->GetLODNum() > 1) { + FString LodGroup_MeshName = MeshNodeName + TEXT("_LodGroup"); + MeshRootNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); TmpNodeNoTransform->AddChild(MeshRootNode); + LodGroup_MeshName = MeshNodeName + TEXT("_LodGroupAttribute"); + FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); + MeshRootNode->AddNodeAttribute(FbxLodGroupAttribute); + + FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true; + //Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group) + for (int CurrentLodIndex = 0; CurrentLodIndex < SkelMesh->GetLODNum(); ++CurrentLodIndex) + { + FString FbxLODNodeName = MeshNodeName + TEXT("_LOD") + FString::FromInt(CurrentLodIndex); + if (CurrentLodIndex + 1 < SkelMesh->GetLODNum()) + { + //Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely + double LodScreenSize = (double)(10.0f / SkelMesh->GetLODInfo(CurrentLodIndex)->ScreenSize.Default); + FbxLodGroupAttribute->AddThreshold(LodScreenSize); + } + FbxNode* FbxActorLOD = CreateMesh(SkelMesh, *FbxLODNodeName, CurrentLodIndex); + if (FbxActorLOD) + { + MeshRootNode->AddChild(FbxActorLOD); + if (SkeletonRootNode) + { + // Bind the mesh to the skeleton + BindMeshToSkeleton(SkelMesh, FbxActorLOD, BoneNodes, CurrentLodIndex); + // Add the bind pose + CreateBindPose(FbxActorLOD); + } + } + } } - - if(SkeletonRootNode && MeshRootNode) + else { - // Bind the mesh to the skeleton - BindMeshToSkeleton(SkelMesh, MeshRootNode, BoneNodes); + MeshRootNode = CreateMesh(SkelMesh, *MeshNodeName, 0); + if (MeshRootNode) + { + TmpNodeNoTransform->AddChild(MeshRootNode); + if (SkeletonRootNode) + { + // Bind the mesh to the skeleton + BindMeshToSkeleton(SkelMesh, MeshRootNode, BoneNodes, 0); - // Add the bind pose - CreateBindPose(MeshRootNode); + // Add the bind pose + CreateBindPose(MeshRootNode); + } + } } if (MeshRootNode) diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxCurveDataImport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxCurveDataImport.cpp index b55a0909904c..f8a38dc3e1ab 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxCurveDataImport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxCurveDataImport.cpp @@ -655,13 +655,13 @@ namespace UnFbx { while (EulerRotXIt && EulerRotYIt && EulerRotZIt) { - float Pitch = EulerRotationY.GetKeyValue(EulerRotYIt.Key()); - float Yaw = EulerRotationZ.GetKeyValue(EulerRotZIt.Key()); - float Roll = EulerRotationX.GetKeyValue(EulerRotXIt.Key());; + float Pitch = EulerRotationY.GetKeyValue(*EulerRotYIt); + float Yaw = EulerRotationZ.GetKeyValue(*EulerRotZIt); + float Roll = EulerRotationX.GetKeyValue(*EulerRotXIt); ConvertRotationToUnreal(Roll, Pitch, Yaw, bIsCamera, bIsLight); - EulerRotationX.SetKeyValue(EulerRotXIt.Key(), Roll, false); - EulerRotationY.SetKeyValue(EulerRotYIt.Key(), Pitch, false); - EulerRotationZ.SetKeyValue(EulerRotZIt.Key(), Yaw, false); + EulerRotationX.SetKeyValue(*EulerRotXIt, Roll, false); + EulerRotationY.SetKeyValue(*EulerRotYIt, Pitch, false); + EulerRotationZ.SetKeyValue(*EulerRotZIt, Yaw, false); ++EulerRotXIt; ++EulerRotYIt; @@ -685,9 +685,9 @@ namespace UnFbx { bool bFirst = true; while (EulerRotXIt && EulerRotYIt && EulerRotZIt) { - float X = EulerRotationX.GetKeyValue(EulerRotXIt.Key());; - float Y = EulerRotationY.GetKeyValue(EulerRotYIt.Key()); - float Z = EulerRotationZ.GetKeyValue(EulerRotZIt.Key()); + float X = EulerRotationX.GetKeyValue(*EulerRotXIt);; + float Y = EulerRotationY.GetKeyValue(*EulerRotYIt); + float Z = EulerRotationZ.GetKeyValue(*EulerRotZIt); if (!bFirst) { @@ -715,9 +715,9 @@ namespace UnFbx { CurrentOutVal[AxisIndex] += CurrentAngleOffset[AxisIndex]; } - EulerRotationX.SetKeyValue(EulerRotXIt.Key(), CurrentOutVal.X, false); - EulerRotationY.SetKeyValue(EulerRotYIt.Key(), CurrentOutVal.Y, false); - EulerRotationZ.SetKeyValue(EulerRotZIt.Key(), CurrentOutVal.Z, false); + EulerRotationX.SetKeyValue(*EulerRotXIt, CurrentOutVal.X, false); + EulerRotationY.SetKeyValue(*EulerRotYIt, CurrentOutVal.Y, false); + EulerRotationZ.SetKeyValue(*EulerRotZIt, CurrentOutVal.Z, false); ++EulerRotXIt; ++EulerRotYIt; diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxFactory.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxFactory.cpp index 2a30d8f89180..b716005826dc 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxFactory.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxFactory.cpp @@ -322,7 +322,6 @@ UObject* UFbxFactory::FactoryCreateFile { ImportOptions->bImportAnimations = false; ImportOptions->bUpdateSkeletonReferencePose = false; - ImportOptions->bUseT0AsRefPose = false; } } @@ -827,7 +826,10 @@ UObject* UFbxFactory::RecursiveImportNode(void* VoidFbxImporter, void* VoidNode, { NewStaticMesh->AddSourceModel(); } - if (LODIndex - 1 > 0 && NewStaticMesh->IsReductionActive(LODIndex-1)) + + ImportANode(VoidFbxImporter, TmpVoidArray, InParent, InName, Flags, NodeIndex, Total, CreatedObject, LODIndex); + + if (LODIndex - 1 > 0 && NewStaticMesh->IsReductionActive(LODIndex - 1)) { //Do not add the LODGroup bias here, since the bias will be apply during the build if (NewStaticMesh->SourceModels[LODIndex - 1].ReductionSettings.PercentTriangles < 1.0f) @@ -858,7 +860,7 @@ UObject* UFbxFactory::RecursiveImportNode(void* VoidFbxImporter, void* VoidNode, NewStaticMesh->SourceModels[LODIndex].bImportWithBaseMesh = true; } } - ImportANode(VoidFbxImporter, TmpVoidArray, InParent, InName, Flags, NodeIndex, Total, CreatedObject, LODIndex); + } } } @@ -1083,8 +1085,9 @@ namespace ImportCompareHelper } } - bool HasConflictRecursive(const FSkeletonTreeNode& ResultAssetRoot, const FSkeletonTreeNode& CurrentAssetRoot) + bool HasRemoveBoneRecursive(const FSkeletonTreeNode& ResultAssetRoot, const FSkeletonTreeNode& CurrentAssetRoot) { + //Find the removed node for (const FSkeletonTreeNode& CurrentNode : CurrentAssetRoot.Childrens) { bool bFoundMatch = false; @@ -1092,7 +1095,29 @@ namespace ImportCompareHelper { if (ResultNode.JointName == CurrentNode.JointName) { - bFoundMatch = !HasConflictRecursive(ResultNode, CurrentNode); + bFoundMatch = !HasRemoveBoneRecursive(ResultNode, CurrentNode); + break; + } + } + if (!bFoundMatch) + { + return true; + } + } + return false; + } + + bool HasAddedBoneRecursive(const FSkeletonTreeNode& ResultAssetRoot, const FSkeletonTreeNode& CurrentAssetRoot) + { + //Find the added node + for (const FSkeletonTreeNode& ResultNode : ResultAssetRoot.Childrens) + { + bool bFoundMatch = false; + for (const FSkeletonTreeNode& CurrentNode : CurrentAssetRoot.Childrens) + { + if (ResultNode.JointName == CurrentNode.JointName) + { + bFoundMatch = !HasAddedBoneRecursive(ResultNode, CurrentNode); break; } } @@ -1106,15 +1131,24 @@ namespace ImportCompareHelper void SetHasConflict(FSkeletonCompareData& SkeletonCompareData) { - SkeletonCompareData.bHasConflict = false; + //Clear the skeleton Result + SkeletonCompareData.CompareResult = ECompareResult::SCR_None; + if (SkeletonCompareData.ResultAssetRoot.JointName != SkeletonCompareData.CurrentAssetRoot.JointName) { - SkeletonCompareData.bHasConflict = true; + SkeletonCompareData.CompareResult = ECompareResult::SCR_SkeletonBadRoot; return; } - //Recursively compare skeleton, return false if there is any unmatch joint. Unmatched joint mean the new skeleton has either joint rename or joint deleted. - SkeletonCompareData.bHasConflict = HasConflictRecursive(SkeletonCompareData.ResultAssetRoot, SkeletonCompareData.CurrentAssetRoot); + if (HasRemoveBoneRecursive(SkeletonCompareData.ResultAssetRoot, SkeletonCompareData.CurrentAssetRoot)) + { + SkeletonCompareData.CompareResult |= ECompareResult::SCR_SkeletonMissingBone; + } + + if (HasAddedBoneRecursive(SkeletonCompareData.ResultAssetRoot, SkeletonCompareData.CurrentAssetRoot)) + { + SkeletonCompareData.CompareResult |= ECompareResult::SCR_SkeletonAddedBone; + } } void FillFbxMaterials(const TArray& MeshNodes, FMaterialCompareData& MaterialCompareData) @@ -1449,15 +1483,15 @@ namespace ImportCompareHelper if (!bImportGeoOnly) { //Fill the currrent asset data - if (ImportUI->Skeleton) + if (ImportUI->Skeleton && SkeletalMesh->Skeleton != ImportUI->Skeleton) { + //In this case we can't use const FReferenceSkeleton& ReferenceSkeleton = ImportUI->Skeleton->GetReferenceSkeleton(); FillRecursivelySkeleton(ReferenceSkeleton, 0, ImportUI->SkeletonCompareData.CurrentAssetRoot); } - else if (SkeletalMesh->Skeleton) + else { - const FReferenceSkeleton& ReferenceSkeleton = SkeletalMesh->Skeleton->GetReferenceSkeleton(); - FillRecursivelySkeleton(ReferenceSkeleton, 0, ImportUI->SkeletonCompareData.CurrentAssetRoot); + FillRecursivelySkeleton(SkeletalMesh->RefSkeleton, 0, ImportUI->SkeletonCompareData.CurrentAssetRoot); } //Fill the result fbx data @@ -1482,7 +1516,16 @@ void UFbxImportUI::UpdateCompareData(UnFbx::FFbxImporter* FbxImporter) MaterialCompareData.Empty(); SkeletonCompareData.Empty(); - const FString Filename = StaticMesh == nullptr ? SkeletalMeshImportData->GetFirstFilename() : StaticMeshImportData->GetFirstFilename(); + FString Filename; + if (StaticMesh != nullptr) + { + Filename = StaticMeshImportData->GetFirstFilename(); + } + else + { + FString FilenameLabel; + SkeletalMeshImportData->GetImportContentFilename(Filename, FilenameLabel); + } if (!FbxImporter->ImportFromFile(*Filename, FPaths::GetExtension(Filename), false)) { diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxMainImport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxMainImport.cpp index 5487092c1747..accf90054dad 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxMainImport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxMainImport.cpp @@ -141,51 +141,53 @@ FBXImportOptions* GetImportOptions( UnFbx::FFbxImporter* FbxImporter, UFbxImport //Make sure the file is open to be able to read the header before showing the options //If the file is already open it will simply return false. - FbxImporter->ReadHeaderFromFile(InFilename, true); - - ImportUI->FileVersion = FbxImporter->GetFbxFileVersion(); - ImportUI->FileCreator = FbxImporter->GetFileCreator(); - // do analytics on getting Fbx data - FbxDocumentInfo* DocInfo = FbxImporter->Scene->GetSceneInfo(); - if (DocInfo) + if (FbxImporter->ReadHeaderFromFile(InFilename, true)) { - FString LastSavedVendor(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVendor.Get().Buffer())); - FString LastSavedAppName(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationName.Get().Buffer())); - FString LastSavedAppVersion(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVersion.Get().Buffer())); - ImportUI->FileCreatorApplication = LastSavedVendor + TEXT(" ") + LastSavedAppName + TEXT(" ") + LastSavedAppVersion; - } - else - { - ImportUI->FileCreatorApplication = TEXT(""); - } + ImportUI->FileVersion = FbxImporter->GetFbxFileVersion(); + ImportUI->FileCreator = FbxImporter->GetFileCreator(); + // do analytics on getting Fbx data + FbxDocumentInfo* DocInfo = FbxImporter->Scene->GetSceneInfo(); + if (DocInfo) + { + FString LastSavedVendor(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVendor.Get().Buffer())); + FString LastSavedAppName(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationName.Get().Buffer())); + FString LastSavedAppVersion(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVersion.Get().Buffer())); - ImportUI->FileUnits = FbxImporter->GetFileUnitSystem(); + ImportUI->FileCreatorApplication = LastSavedVendor + TEXT(" ") + LastSavedAppName + TEXT(" ") + LastSavedAppVersion; + } + else + { + ImportUI->FileCreatorApplication = TEXT(""); + } - ImportUI->FileAxisDirection = FbxImporter->GetFileAxisDirection(); + ImportUI->FileUnits = FbxImporter->GetFileUnitSystem(); - //Set the info original file frame rate - ImportUI->FileSampleRate = FString::Printf(TEXT("%.2f"), FbxImporter->GetOriginalFbxFramerate()); + ImportUI->FileAxisDirection = FbxImporter->GetFileAxisDirection(); - //Set the info start time and the end time - ImportUI->AnimStartFrame = TEXT("0"); - ImportUI->AnimEndFrame = TEXT("0"); - FbxTimeSpan AnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE); - int32 AnimStackCount = FbxImporter->Scene->GetSrcObjectCount(); - for (int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++) - { - FbxAnimStack* CurAnimStack = FbxImporter->Scene->GetSrcObject(AnimStackIndex); - FbxTimeSpan AnimatedInterval(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE); - FbxImporter->GetAnimationIntervalMultiLayer(FbxImporter->Scene->GetRootNode(), CurAnimStack, AnimatedInterval); - // find the most range that covers by both method, that'll be used for clamping - AnimTimeSpan.SetStart(FMath::Min(AnimTimeSpan.GetStart(), AnimatedInterval.GetStart())); - AnimTimeSpan.SetStop(FMath::Max(AnimTimeSpan.GetStop(), AnimatedInterval.GetStop())); - } - if (AnimTimeSpan.GetStart() != FBXSDK_TIME_INFINITE) - { - FbxTime EachFrame = FBXSDK_TIME_ONE_SECOND / FbxImporter->GetOriginalFbxFramerate(); - ImportUI->AnimStartFrame = FString::FromInt(AnimTimeSpan.GetStart().Get() / EachFrame.Get()); - ImportUI->AnimEndFrame = FString::FromInt(AnimTimeSpan.GetStop().Get() / EachFrame.Get()); + //Set the info original file frame rate + ImportUI->FileSampleRate = FString::Printf(TEXT("%.2f"), FbxImporter->GetOriginalFbxFramerate()); + + //Set the info start time and the end time + ImportUI->AnimStartFrame = TEXT("0"); + ImportUI->AnimEndFrame = TEXT("0"); + FbxTimeSpan AnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE); + int32 AnimStackCount = FbxImporter->Scene->GetSrcObjectCount(); + for (int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++) + { + FbxAnimStack* CurAnimStack = FbxImporter->Scene->GetSrcObject(AnimStackIndex); + FbxTimeSpan AnimatedInterval(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE); + FbxImporter->Scene->GetRootNode()->GetAnimationInterval(AnimatedInterval, CurAnimStack); + // find the most range that covers by both method, that'll be used for clamping + AnimTimeSpan.SetStart(FMath::Min(AnimTimeSpan.GetStart(), AnimatedInterval.GetStart())); + AnimTimeSpan.SetStop(FMath::Max(AnimTimeSpan.GetStop(), AnimatedInterval.GetStop())); + } + if (AnimTimeSpan.GetStart() != FBXSDK_TIME_INFINITE) + { + FbxTime EachFrame = FBXSDK_TIME_ONE_SECOND / FbxImporter->GetOriginalFbxFramerate(); + ImportUI->AnimStartFrame = FString::FromInt(AnimTimeSpan.GetStart().Get() / EachFrame.Get()); + ImportUI->AnimEndFrame = FString::FromInt(AnimTimeSpan.GetStop().Get() / EachFrame.Get()); + } } if (ImportUI->MeshTypeToImport != FBXIT_Animation && ImportUI->ReimportMesh != nullptr) @@ -213,9 +215,9 @@ FBXImportOptions* GetImportOptions( UnFbx::FFbxImporter* FbxImporter, UFbxImport } }); - ImportUI->OnShowSkeletonConflictDialog = FOnShowConflictDialog::CreateLambda([&ImportUI, &FbxImporter] + ImportUI->OnShowSkeletonConflictDialog = FOnShowConflictDialog::CreateLambda([&ImportUI, &FbxImporter]() { - if (!ImportUI->SkeletonCompareData.bHasConflict) + if (ImportUI->SkeletonCompareData.CompareResult == ImportCompareHelper::ECompareResult::SCR_None) { return; } @@ -1241,6 +1243,7 @@ bool FFbxImporter::ImportFile(FString Filename, bool bPreventMaterialNameClash / ReleaseScene(); Result = false; CurPhase = NOTSTARTED; + return Result; } const FbxGlobalSettings& GlobalSettings = Scene->GetGlobalSettings(); diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxOptionWindow.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxOptionWindow.cpp index a53eff042da5..56e50057398a 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxOptionWindow.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxOptionWindow.cpp @@ -52,7 +52,7 @@ void SFbxOptionWindow::Construct(const FArguments& InArgs) [ SNew(STextBlock) .Font(FEditorStyle::GetFontStyle("CurveEd.LabelFont")) - .Text(LOCTEXT("Import_CurrentFileTitle", "Current File: ")) + .Text(LOCTEXT("Import_CurrentFileTitle", "Current Asset: ")) ] +SHorizontalBox::Slot() .Padding(5, 0, 0, 0) @@ -62,6 +62,7 @@ void SFbxOptionWindow::Construct(const FArguments& InArgs) SNew(STextBlock) .Font(FEditorStyle::GetFontStyle("CurveEd.InfoFont")) .Text(InArgs._FullPath) + .ToolTipText(InArgs._FullPath) ] ] ] diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshExport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshExport.cpp index d614c07f9726..8b52ac6b7f2c 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshExport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshExport.cpp @@ -103,10 +103,16 @@ void FFbxExporter::GetSkeleton(FbxNode* RootNode, TArray& BoneNodes) /** * Adds an Fbx Mesh to the FBX scene based on the data in the given FSkeletalMeshLODModel */ -FbxNode* FFbxExporter::CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* MeshName) +FbxNode* FFbxExporter::CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* MeshName, int32 LODIndex) { const FSkeletalMeshModel* SkelMeshResource = SkelMesh->GetImportedModel(); - const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[0]; + if (!SkelMeshResource->LODModels.IsValidIndex(LODIndex)) + { + //Return an empty node + return FbxNode::Create(Scene, TCHAR_TO_UTF8(MeshName)); + } + + const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[LODIndex]; const int32 VertexCount = SourceModel.GetNumNonClothingVertices(); // Verify the integrity of the mesh. @@ -250,18 +256,26 @@ FbxNode* FFbxExporter::CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* Me UMaterialInterface* MatInterface = SkelMesh->Materials[MaterialIndex].MaterialInterface; FbxSurfaceMaterial* FbxMaterial = NULL; - if(MatInterface && !FbxMaterials.Find(MatInterface)) + if (LODIndex == 0) { - FbxMaterial = ExportMaterial(MatInterface); + if (MatInterface && !FbxMaterials.Find(MatInterface)) + { + FbxMaterial = ExportMaterial(MatInterface); + } } - else + else if(MatInterface) + { + FbxMaterial = *(FbxMaterials.Find(MatInterface)); + } + + if(!FbxMaterial) { // Note: The vertex data relies on there being a set number of Materials. // If you try to add the same material again it will not be added, so create a // default material with a unique name to ensure the proper number of materials - TCHAR NewMaterialName[MAX_SPRINTF]=TEXT(""); - FCString::Sprintf( NewMaterialName, TEXT("Fbx Default Material %i"), MaterialIndex ); + TCHAR NewMaterialName[MAX_SPRINTF] = TEXT(""); + FCString::Sprintf(NewMaterialName, TEXT("Fbx Default Material %i"), MaterialIndex); FbxMaterial = FbxSurfaceLambert::Create(Scene, TCHAR_TO_UTF8(NewMaterialName)); ((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(FbxDouble3(0.72, 0.72, 0.72)); @@ -280,10 +294,15 @@ FbxNode* FFbxExporter::CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* Me /** * Adds Fbx Clusters necessary to skin a skeletal mesh to the bones in the BoneNodes list */ -void FFbxExporter::BindMeshToSkeleton(const USkeletalMesh* SkelMesh, FbxNode* MeshRootNode, TArray& BoneNodes) +void FFbxExporter::BindMeshToSkeleton(const USkeletalMesh* SkelMesh, FbxNode* MeshRootNode, TArray& BoneNodes, int32 LODIndex) { const FSkeletalMeshModel* SkelMeshResource = SkelMesh->GetImportedModel(); - const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[0]; + if (!SkelMeshResource->LODModels.IsValidIndex(LODIndex)) + { + //We cannot bind the LOD if its not valid + return; + } + const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[LODIndex]; const int32 VertexCount = SourceModel.NumVertices; FbxAMatrix MeshMatrix; @@ -525,11 +544,11 @@ void ExportObjectMetadataToBones(const UObject* ObjectToExport, const TArray BoneNodes; // Add the skeleton to the scene - FbxNode* SkeletonRootNode = CreateSkeleton(SkelMesh, BoneNodes); + FbxNode* SkeletonRootNode = CreateSkeleton(SkeletalMesh, BoneNodes); if(SkeletonRootNode) { TmpNodeNoTransform->AddChild(SkeletonRootNode); } - // Add the mesh - FbxNode* MeshRootNode = CreateMesh(SkelMesh, MeshName); - if(MeshRootNode) + FbxNode* MeshRootNode = nullptr; + if (GetExportOptions()->LevelOfDetail && SkeletalMesh->GetLODNum() > 1) { + FString LodGroup_MeshName = FString(MeshName) + TEXT("_LodGroup"); + MeshRootNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); TmpNodeNoTransform->AddChild(MeshRootNode); + LodGroup_MeshName = FString(MeshName) + TEXT("_LodGroupAttribute"); + FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); + MeshRootNode->AddNodeAttribute(FbxLodGroupAttribute); + + FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true; + //Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group) + for (int CurrentLodIndex = 0; CurrentLodIndex < SkeletalMesh->GetLODNum(); ++CurrentLodIndex) + { + FString FbxLODNodeName = FString(MeshName) + TEXT("_LOD") + FString::FromInt(CurrentLodIndex); + if (CurrentLodIndex + 1 < SkeletalMesh->GetLODNum()) + { + //Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely + double LodScreenSize = (double)(10.0f / SkeletalMesh->GetLODInfo(CurrentLodIndex)->ScreenSize.Default); + FbxLodGroupAttribute->AddThreshold(LodScreenSize); + } + FbxNode* FbxActorLOD = CreateMesh(SkeletalMesh, *FbxLODNodeName, CurrentLodIndex); + if (FbxActorLOD) + { + MeshRootNode->AddChild(FbxActorLOD); + if (SkeletonRootNode) + { + // Bind the mesh to the skeleton + BindMeshToSkeleton(SkeletalMesh, FbxActorLOD, BoneNodes, CurrentLodIndex); + // Add the bind pose + CreateBindPose(FbxActorLOD); + } + } + } } - - if(SkeletonRootNode && MeshRootNode) + else { - // Bind the mesh to the skeleton - BindMeshToSkeleton(SkelMesh, MeshRootNode, BoneNodes); + MeshRootNode = CreateMesh(SkeletalMesh, MeshName, 0); + if (MeshRootNode) + { + TmpNodeNoTransform->AddChild(MeshRootNode); + if (SkeletonRootNode) + { + // Bind the mesh to the skeleton + BindMeshToSkeleton(SkeletalMesh, MeshRootNode, BoneNodes, 0); - // Add the bind pose - CreateBindPose(MeshRootNode); + // Add the bind pose + CreateBindPose(MeshRootNode); + } + } } if (SkeletonRootNode) @@ -572,13 +627,13 @@ FbxNode* FFbxExporter::ExportSkeletalMeshToFbx(const USkeletalMesh* SkelMesh, co ActorRootNode->AddChild(SkeletonRootNode); } - ExportObjectMetadataToBones(SkelMesh->Skeleton, BoneNodes); + ExportObjectMetadataToBones(SkeletalMesh->Skeleton, BoneNodes); if (MeshRootNode) { TmpNodeNoTransform->RemoveChild(MeshRootNode); ActorRootNode->AddChild(MeshRootNode); - ExportObjectMetadata(SkelMesh, MeshRootNode); + ExportObjectMetadata(SkeletalMesh, MeshRootNode); } Scene->GetRootNode()->RemoveChild(TmpNodeNoTransform); diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImport.cpp index c146cf84359f..0ba6bfd27340 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImport.cpp @@ -757,7 +757,7 @@ bool UnFbx::FFbxImporter::RetrievePoseFromBindPose(const TArray& NodeA return (PoseArray.Size() > 0); } -bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshImportData &ImportData, UFbxSkeletalMeshImportData* TemplateData ,TArray &SortedLinks, bool& bOutDiffPose, bool bDisableMissingBindPoseWarning, bool & bUseTime0AsRefPose, FbxNode *SkeletalMeshNode) +bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshImportData &ImportData, UFbxSkeletalMeshImportData* TemplateData ,TArray &SortedLinks, bool& bOutDiffPose, bool bDisableMissingBindPoseWarning, bool & bUseTime0AsRefPose, FbxNode *SkeletalMeshNode, bool bIsReimport) { bOutDiffPose = false; int32 SkelType = 0; // 0 for skeletal mesh, 1 for rigid mesh @@ -765,6 +765,7 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI FbxArray PoseArray; TArray ClusterArray; + bool AllowImportBoneErrorAndWarning = !(bIsReimport && ImportOptions->bImportAsSkeletalGeometry); if (NodeArray[0]->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) == 0) { SkelType = 1; @@ -784,7 +785,7 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI //if we created missing bind poses, update the number of bind poses int32 NbPoses = SdkManager->GetBindPoseCount(Scene); - if ( NbPoses != Default_NbPoses) + if ( NbPoses != Default_NbPoses && AllowImportBoneErrorAndWarning) { AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_SceneMissingBinding", "The imported scene has no initial binding position (Bind Pose) for the skin. The plug-in will compute one automatically. However, it may create unexpected results.")), FFbxErrors::SkeletalMesh_NoBindPoseInScene); } @@ -809,7 +810,10 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI if (ClusterArray.Num() == 0) { - AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_NoAssociatedCluster", "No associated clusters")), FFbxErrors::SkeletalMesh_NoAssociatedCluster); + if (AllowImportBoneErrorAndWarning) + { + AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_NoAssociatedCluster", "No associated clusters")), FFbxErrors::SkeletalMesh_NoAssociatedCluster); + } return false; } @@ -853,7 +857,10 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI // if no bond is found if(SortedLinks.Num() == 0) { - AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NoBone", "'{0}' has no bones"), FText::FromString(NodeArray[0]->GetName()))), FFbxErrors::SkeletalMesh_NoBoneFound); + if (AllowImportBoneErrorAndWarning) + { + AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NoBone", "'{0}' has no bones"), FText::FromString(NodeArray[0]->GetName()))), FFbxErrors::SkeletalMesh_NoBoneFound); + } return false; } @@ -888,9 +895,12 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI if(FCStringAnsi::Strcmp(Link->GetName(), AltLink->GetName()) == 0) { - FString RawBoneName = UTF8_TO_TCHAR(Link->GetName()); - AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_DuplicateBoneName", "Error, Could not import {0}.\nDuplicate bone name found ('{1}'). Each bone must have a unique name."), - FText::FromString(NodeArray[0]->GetName()), FText::FromString(RawBoneName))), FFbxErrors::SkeletalMesh_DuplicateBones); + if (AllowImportBoneErrorAndWarning) + { + FString RawBoneName = UTF8_TO_TCHAR(Link->GetName()); + AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_DuplicateBoneName", "Error, Could not import {0}.\nDuplicate bone name found ('{1}'). Each bone must have a unique name."), + FText::FromString(NodeArray[0]->GetName()), FText::FromString(RawBoneName))), FFbxErrors::SkeletalMesh_DuplicateBones); + } return false; } } @@ -942,7 +952,10 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI RootIdx = LinkIndex; if (NumberOfRoot > 1) { - AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("MultipleRootsFound", "Multiple roots are found in the bone hierarchy. We only support single root bone.")), FFbxErrors::SkeletalMesh_MultipleRoots); + if (AllowImportBoneErrorAndWarning) + { + AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("MultipleRootsFound", "Multiple roots are found in the bone hierarchy. We only support single root bone.")), FFbxErrors::SkeletalMesh_MultipleRoots); + } return false; } } @@ -1090,7 +1103,7 @@ bool UnFbx::FFbxImporter::ImportBone(TArray& NodeArray, FSkeletalMeshI RootTransform.SetFromMatrix(RootTransform.ToMatrixWithScale() * AddedMatrix); } - if(bAnyLinksNotInBindPose) + if(bAnyLinksNotInBindPose && AllowImportBoneErrorAndWarning) { AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_BonesAreMissingFromBindPose", "The following bones are missing from the bind pose:\n{0}\nThis can happen for bones that are not vert weighted. If they are not in the correct orientation after importing,\nplease set the \"Use T0 as ref pose\" option or add them to the bind pose and reimport the skeletal mesh."), FText::FromString(LinksWithoutBindPoses))), FFbxErrors::SkeletalMesh_BonesAreMissingFromBindPose); } @@ -1131,9 +1144,9 @@ bool UnFbx::FFbxImporter::FillSkeletalMeshImportData(TArray& NodeArray SkelMeshImportDataPtr->bUseT0AsRefPose = ImportOptions->bUseT0AsRefPose; // Note: importing morph data causes additional passes through this function, so disable the warning dialogs // from popping up again on each additional pass. - if (!bIsReimport || !ImportOptions->bImportAsSkeletalGeometry) //Do not import bone if we import only the geometry and we are reimporting + if (!ImportBone(NodeArray, *SkelMeshImportDataPtr, TemplateImportData, SortedLinkArray, SkelMeshImportDataPtr->bDiffPose, (FbxShapeArray != nullptr), SkelMeshImportDataPtr->bUseT0AsRefPose, Node, bIsReimport)) { - if (!ImportBone(NodeArray, *SkelMeshImportDataPtr, TemplateImportData, SortedLinkArray, SkelMeshImportDataPtr->bDiffPose, (FbxShapeArray != nullptr), SkelMeshImportDataPtr->bUseT0AsRefPose, Node)) + if (!(bIsReimport && ImportOptions->bImportAsSkeletalGeometry)) //Do not import bone if we import only the geometry and we are reimporting { AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("FbxSkeletaLMeshimport_MultipleRootFound", "Multiple roots found")), FFbxErrors::SkeletalMesh_MultipleRoots); return false; @@ -1607,7 +1620,8 @@ USkeletalMesh* UnFbx::FFbxImporter::ImportSkeletalMesh(FImportSkeletalMeshArgs & // process reference skeleton from import data int32 SkeletalDepth = 0; - if (!ProcessImportMeshSkeleton(SkeletalMesh->Skeleton, SkeletalMesh->RefSkeleton, SkeletalDepth, *SkelMeshImportDataPtr)) + USkeleton* ExistingSkeleton = ExistSkelMeshDataPtr != nullptr ? ExistSkelMeshDataPtr->ExistingSkeleton : SkeletalMesh->Skeleton; + if (!ProcessImportMeshSkeleton(ExistingSkeleton, SkeletalMesh->RefSkeleton, SkeletalDepth, *SkelMeshImportDataPtr)) { SkeletalMesh->ClearFlags(RF_Standalone); SkeletalMesh->Rename(NULL, GetTransientPackage()); @@ -1707,9 +1721,25 @@ USkeletalMesh* UnFbx::FFbxImporter::ImportSkeletalMesh(FImportSkeletalMeshArgs & // Store the current file path and timestamp for re-import purposes UFbxSkeletalMeshImportData* ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, ImportSkeletalMeshArgs.TemplateImportData); - SkeletalMesh->AssetImportData->Update(UFactory::GetCurrentFilename(), &Md5Hash); - + if (ExistingSkelMesh) + { + //In case of a re-import we update only the type we re-import + SkeletalMesh->AssetImportData->AddFileName(UFactory::GetCurrentFilename(), ImportOptions->bImportAsSkeletalGeometry ? 1 : ImportOptions->bImportAsSkeletalSkinning ? 2 : 0); + } + else + { + //When we create a new skeletal mesh asset we create 1 or 3 source files. 1 in case we import all the content, 3 in case we import geo or skinning + int32 SourceFileIndex = ImportOptions->bImportAsSkeletalGeometry ? 1 : ImportOptions->bImportAsSkeletalSkinning ? 2 : 0; + //Always add the base sourcefile + SkeletalMesh->AssetImportData->AddFileName(UFactory::GetCurrentFilename(), 0, NSSkeletalMeshSourceFileLabels::GeoAndSkinningText().ToString()); + if (SourceFileIndex != 0) + { + //If the user import geo or skinning, we add both entries to allow reimport of them + SkeletalMesh->AssetImportData->AddFileName(UFactory::GetCurrentFilename(), 1, NSSkeletalMeshSourceFileLabels::GeometryText().ToString()); + SkeletalMesh->AssetImportData->AddFileName(UFactory::GetCurrentFilename(), 2, NSSkeletalMeshSourceFileLabels::SkinningText().ToString()); + } + } if (ExistSkelMeshDataPtr) { @@ -1908,6 +1938,9 @@ void UnFbx::FFbxImporter::UpdateSkeletalMeshImportData(USkeletalMesh *SkeletalMe } if (ImportData) { + //Set the last content type import + ImportData->LastImportContentType = ImportData->ImportContentType; + ImportData->ImportMaterialOriginalNameData.Empty(); if (ImportMaterialOriginalNameData && ImportMeshLodData) { @@ -3463,8 +3496,19 @@ void UnFbx::FFbxImporter::InsertNewLODToBaseSkeletalMesh(USkeletalMesh* InSkelet //Get the Last imported skelmesh section slot import name OriginalImportMeshSectionSlotNames.Add(OriginalImportMeshLodSectionsData.SectionOriginalMaterialName[SectionIndex]); + //We have some old LodModel data to restore + if (DestImportedResource->LODModels.IsValidIndex(DesiredLOD) + && DestImportedResource->LODModels[DesiredLOD].Sections.IsValidIndex(SectionIndex) + && NewLODModel.Sections.IsValidIndex(SectionIndex)) + { + NewLODModel.Sections[SectionIndex].bCastShadow = DestImportedResource->LODModels[DesiredLOD].Sections[SectionIndex].bCastShadow; + NewLODModel.Sections[SectionIndex].bRecomputeTangent = DestImportedResource->LODModels[DesiredLOD].Sections[SectionIndex].bRecomputeTangent; + NewLODModel.Sections[SectionIndex].bDisabled = DestImportedResource->LODModels[DesiredLOD].Sections[SectionIndex].bDisabled; + NewLODModel.Sections[SectionIndex].GenerateUpToLodIndex = DestImportedResource->LODModels[DesiredLOD].Sections[SectionIndex].GenerateUpToLodIndex; + } } } + } // If we want to add this as a new LOD to this mesh - add to LODModels/LODInfo array. diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImportData.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImportData.cpp index 672dd14ad250..2f6fb525a309 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImportData.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxSkeletalMeshImportData.cpp @@ -2,6 +2,7 @@ #include "Factories/FbxSkeletalMeshImportData.h" #include "Engine/SkeletalMesh.h" +#include "UObject/Package.h" UFbxSkeletalMeshImportData::UFbxSkeletalMeshImportData(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -10,6 +11,7 @@ UFbxSkeletalMeshImportData::UFbxSkeletalMeshImportData(const FObjectInitializer& bTransformVertexToAbsolute = true; bBakePivotInVertex = false; VertexColorImportOption = EVertexColorImportOption::Replace; + LastImportContentType = EFBXImportContentType::FBXICT_All; } UFbxSkeletalMeshImportData* UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(USkeletalMesh* SkeletalMesh, UFbxSkeletalMeshImportData* TemplateForCreation) @@ -44,3 +46,47 @@ bool UFbxSkeletalMeshImportData::CanEditChange(const UProperty* InProperty) cons } return bMutable; } + +bool UFbxSkeletalMeshImportData::GetImportContentFilename(FString& OutFilename, FString& OutFilenameLabel) const +{ + if (SourceData.SourceFiles.Num() < 1) + { + OutFilename = FString(); + OutFilenameLabel = FString(); + return false; + } + int32 SourceIndex = ImportContentType == EFBXImportContentType::FBXICT_All ? 0 : ImportContentType == EFBXImportContentType::FBXICT_Geometry ? 1 : 2; + if (SourceData.SourceFiles.Num() > SourceIndex) + { + OutFilename = ResolveImportFilename(SourceData.SourceFiles[SourceIndex].RelativeFilename); + OutFilenameLabel = SourceData.SourceFiles[SourceIndex].DisplayLabelName; + return true; + } + OutFilename = ResolveImportFilename(SourceData.SourceFiles[0].RelativeFilename); + OutFilenameLabel = SourceData.SourceFiles[0].DisplayLabelName; + return true; +} + +void UFbxSkeletalMeshImportData::AppendAssetRegistryTags(TArray& OutTags) +{ + auto EFBXImportContentTypeToString = [](const EFBXImportContentType value)-> FString + { + switch (value) + { + case EFBXImportContentType::FBXICT_All: + return TEXT("FBXICT_All"); + case EFBXImportContentType::FBXICT_Geometry: + return TEXT("FBXICT_Geometry"); + case EFBXImportContentType::FBXICT_SkinningWeights: + return TEXT("FBXICT_SkinningWeights"); + case EFBXImportContentType::FBXICT_MAX: + return TEXT("FBXICT_MAX"); + } + return TEXT("FBXICT_All"); + }; + + FString EnumString = EFBXImportContentTypeToString(LastImportContentType); + OutTags.Add(FAssetRegistryTag("LastImportContentType", EnumString, FAssetRegistryTag::TT_Hidden)); + + Super::AppendAssetRegistryTags(OutTags); +} \ No newline at end of file diff --git a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxStaticMeshImport.cpp b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxStaticMeshImport.cpp index f5e828529726..4e1e27125aa4 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxStaticMeshImport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxStaticMeshImport.cpp @@ -1485,6 +1485,11 @@ UStaticMesh* UnFbx::FFbxImporter::ImportStaticMeshAsSingle(UObject* InParent, TA MeshDescription = StaticMesh->CreateMeshDescription(LODIndex); check(MeshDescription != nullptr); StaticMesh->CommitMeshDescription(LODIndex); + //Make sure an imported mesh do not get reduce if there was no mesh data before reimport. + //In this case we have a generated LOD convert to a custom LOD + StaticMesh->SourceModels[LODIndex].ReductionSettings.MaxDeviation = 0.0f; + StaticMesh->SourceModels[LODIndex].ReductionSettings.PercentTriangles = 1.0f; + StaticMesh->SourceModels[LODIndex].ReductionSettings.PercentVertices = 1.0f; } else if (InStaticMesh != NULL && LODIndex > 0) { diff --git a/Engine/Source/Editor/UnrealEd/Private/FbxAnimUtils.cpp b/Engine/Source/Editor/UnrealEd/Private/FbxAnimUtils.cpp index 8af353a647e9..1b4ec0d15952 100644 --- a/Engine/Source/Editor/UnrealEd/Private/FbxAnimUtils.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/FbxAnimUtils.cpp @@ -128,7 +128,7 @@ namespace FbxAnimUtils if (FbxNode* MeshNode = FindCurveNode(FbxImporter, InCurveNodeName, FbxNodeAttribute::eMesh)) { // We have a node, so clear the curve table - InOutCurveTable->RowMap.Empty(); + InOutCurveTable->EmptyTable(); FbxGeometry* Geometry = (FbxGeometry*)MeshNode->GetNodeAttribute(); int32 BlendShapeDeformerCount = Geometry->GetDeformerCount(FbxDeformer::eBlendShape); @@ -157,7 +157,7 @@ namespace FbxAnimUtils FbxAnimCurve* Curve = Geometry->GetShapeChannel(BlendShapeIndex, ChannelIndex, (FbxAnimLayer*)AnimStack->GetMember(0)); if (Curve) { - FRichCurve& RichCurve = *InOutCurveTable->RowMap.Add(*ChannelName, new FRichCurve()); + FRichCurve& RichCurve = InOutCurveTable->AddRichCurve(*ChannelName); RichCurve.Reset(); FbxImporter->ImportCurve(Curve, RichCurve, AnimTimeSpan, 0.01f); @@ -171,7 +171,7 @@ namespace FbxAnimUtils else if (FbxNode* SkeletonNode = FindCurveNode(FbxImporter, InCurveNodeName, FbxNodeAttribute::eSkeleton)) { // We have a node, so clear the curve table - InOutCurveTable->RowMap.Empty(); + InOutCurveTable->EmptyTable(); ExtractAttributeCurves(SkeletonNode, false, [&InOutCurveTable, &AnimTimeSpan, &FbxImporter](FbxAnimCurve* InCurve, const FString& InCurveName, const FString& InChannelName, int32 InChannelIndex, int32 InNumChannels) { @@ -185,7 +185,7 @@ namespace FbxAnimUtils FinalCurveName = InCurveName + "_" + InChannelName; } - FRichCurve& RichCurve = *InOutCurveTable->RowMap.Add(*FinalCurveName, new FRichCurve()); + FRichCurve& RichCurve = InOutCurveTable->AddRichCurve(*FinalCurveName); RichCurve.Reset(); FbxImporter->ImportCurve(InCurve, RichCurve, AnimTimeSpan, 1.0f); diff --git a/Engine/Source/Editor/UnrealEd/Private/FbxExporter.h b/Engine/Source/Editor/UnrealEd/Private/FbxExporter.h index ab5f06782f3e..0b02060a23bf 100644 --- a/Engine/Source/Editor/UnrealEd/Private/FbxExporter.h +++ b/Engine/Source/Editor/UnrealEd/Private/FbxExporter.h @@ -356,12 +356,12 @@ private: /** * Adds an Fbx Mesh to the FBX scene based on the data in the given FSkeletalMeshLODModel */ - FbxNode* CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* MeshName); + FbxNode* CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* MeshName, int32 LODIndex); /** * Adds Fbx Clusters necessary to skin a skeletal mesh to the bones in the BoneNodes list */ - void BindMeshToSkeleton(const USkeletalMesh* SkelMesh, FbxNode* MeshRootNode, TArray& BoneNodes); + void BindMeshToSkeleton(const USkeletalMesh* SkelMesh, FbxNode* MeshRootNode, TArray& BoneNodes, int32 LODIndex); /** * Add a bind pose to the scene based on the FbxMesh and skinning settings of the given node diff --git a/Engine/Source/Editor/UnrealEd/Private/FbxMeshUtils.cpp b/Engine/Source/Editor/UnrealEd/Private/FbxMeshUtils.cpp index 6558478c64a5..6e4518d21483 100644 --- a/Engine/Source/Editor/UnrealEd/Private/FbxMeshUtils.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/FbxMeshUtils.cpp @@ -117,6 +117,8 @@ namespace FbxMeshUtils ImportOptions->bImportMaterials = false; ImportOptions->bImportTextures = false; + //Make sure the LODGroup do not change when re-importing a mesh + ImportOptions->StaticMeshLODGroup = BaseStaticMesh->LODGroup; } ImportOptions->bAutoComputeLodDistances = true; //Setting auto compute distance to true will avoid changing the staticmesh flag if ( !FFbxImporter->ImportFromFile( *Filename, FPaths::GetExtension( Filename ), true ) ) @@ -331,7 +333,6 @@ namespace FbxMeshUtils { ImportOptions->bImportAnimations = false; ImportOptions->bUpdateSkeletonReferencePose = false; - ImportOptions->bUseT0AsRefPose = false; } if ( !FFbxImporter->ImportFromFile( *Filename, FPaths::GetExtension( Filename ), true ) ) diff --git a/Engine/Source/Editor/UnrealEd/Private/FileHelpers.cpp b/Engine/Source/Editor/UnrealEd/Private/FileHelpers.cpp index bd9ac4312f89..95539f4c4634 100644 --- a/Engine/Source/Editor/UnrealEd/Private/FileHelpers.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/FileHelpers.cpp @@ -228,7 +228,8 @@ static bool InInterpEditMode() */ static bool ConfirmPackageBranchCheckOutStatus(const TArray& PackagesToCheckOut) { - for (auto& CurPackage : PackagesToCheckOut) + //@TODO: Need more info here (in the event multiple packages are trying to be saved at once; the prompt shown is misleading in that case (you might be OK with stomping over one file but not others later on in the list)) + for (UPackage* CurPackage : PackagesToCheckOut) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPackage, EStateCacheUsage::Use); @@ -280,8 +281,10 @@ static bool ConfirmPackageBranchCheckOutStatus(const TArray& Packages } - const FText Message = SourceControlState->IsModifiedInOtherBranch() ? FText::Format(LOCTEXT("WarningModifiedOtherBranch", "WARNING: Packages modified in {0} CL {1}\n\n{2}\n\nCheck out packages anyway?"), FText::FromString(HeadBranch), FText::AsNumber(HeadCL, &NoCommas), InfoText) - : FText::Format(LOCTEXT("WarningCheckedOutOtherBranch", "WARNING: Packages checked out in {0}\n\n{1}\n\nCheck out packages anyway?"), FText::FromString(SourceControlState->GetOtherUserBranchCheckedOuts()), InfoText); + const FText PackageNameText = FText::FromName(CurPackage->GetFName()); + + const FText Message = SourceControlState->IsModifiedInOtherBranch() ? FText::Format(LOCTEXT("WarningModifiedOtherBranch", "WARNING: Package {3} modified in {0} CL {1}\n\n{2}\n\nCheck out packages anyway?"), FText::FromString(HeadBranch), FText::AsNumber(HeadCL, &NoCommas), InfoText, PackageNameText) + : FText::Format(LOCTEXT("WarningCheckedOutOtherBranch", "WARNING: Package {2} checked out in {0}\n\n{1}\n\nCheck out packages anyway?"), FText::FromString(SourceControlState->GetOtherUserBranchCheckedOuts()), InfoText, PackageNameText); return OpenMsgDlgInt(EAppMsgType::YesNo, Message, SourceControlState->IsModifiedInOtherBranch() ? FText::FromString("Package Branch Modifications") : FText::FromString("Package Branch Checkouts")) == EAppReturnType::Yes; } diff --git a/Engine/Source/Editor/UnrealEd/Private/Kismet2/BlueprintEditorUtils.cpp b/Engine/Source/Editor/UnrealEd/Private/Kismet2/BlueprintEditorUtils.cpp index b7976213e412..a1ff189a9515 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Kismet2/BlueprintEditorUtils.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Kismet2/BlueprintEditorUtils.cpp @@ -2808,6 +2808,9 @@ void FBlueprintEditorUtils::RenameGraph(UEdGraph* Graph, const FString& NewNameS } } + // We should let the blueprint know we renamed a graph, some stuff may need to be fixed up. + Blueprint->NotifyGraphRenamed(Graph, OldGraphName, NewGraphName); + if (!Blueprint->bIsRegeneratingOnLoad && !Blueprint->bBeingCompiled) { FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); diff --git a/Engine/Source/Editor/UnrealEd/Private/LevelEditorViewport.cpp b/Engine/Source/Editor/UnrealEd/Private/LevelEditorViewport.cpp index 702fde6f3d1b..311fbc0a4550 100644 --- a/Engine/Source/Editor/UnrealEd/Private/LevelEditorViewport.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/LevelEditorViewport.cpp @@ -3261,11 +3261,17 @@ void FLevelEditorViewportClient::MoveCameraToLockedActor() } USceneComponent* FLevelEditorViewportClient::FindViewComponentForActor(AActor const* Actor) +{ + TSet CheckedActors; + return FindViewComponentForActor(Actor, CheckedActors); +} + +USceneComponent* FLevelEditorViewportClient::FindViewComponentForActor(AActor const* Actor, TSet& CheckedActors) { USceneComponent* PreviewComponent = nullptr; - if (Actor) + if (Actor && !CheckedActors.Contains(Actor)) { - + CheckedActors.Add(Actor); // see if actor has a component with preview capabilities (prioritize camera components) TArray SceneComps; Actor->GetComponents(SceneComps); @@ -3301,14 +3307,13 @@ USceneComponent* FLevelEditorViewportClient::FindViewComponentForActor(AActor co // now see if any actors are attached to us, directly or indirectly, that have an active camera component we might want to use // we will just return the first one. - // #note: assumption here that attachment cannot be circular if (PreviewComponent == nullptr) { TArray AttachedActors; Actor->GetAttachedActors(AttachedActors); for (AActor* AttachedActor : AttachedActors) { - USceneComponent* const Comp = FindViewComponentForActor(AttachedActor); + USceneComponent* const Comp = FindViewComponentForActor(AttachedActor, CheckedActors); if (Comp) { PreviewComponent = Comp; diff --git a/Engine/Source/Editor/UnrealEd/Private/Lightmass/Lightmass.cpp b/Engine/Source/Editor/UnrealEd/Private/Lightmass/Lightmass.cpp index a87670c283b2..8cca540296f5 100644 --- a/Engine/Source/Editor/UnrealEd/Private/Lightmass/Lightmass.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/Lightmass/Lightmass.cpp @@ -2944,19 +2944,13 @@ bool FLightmassProcessor::BeginRun() // Set up optional dependencies. These might not exist in Launcher distributions, for example. const TCHAR* OptionalDependencyPaths32[] = { - TEXT("../Win32/UnrealLightmass.pdb"), - TEXT("../DotNET/AutoReporter.exe"), - TEXT("../DotNET/AutoReporter.exe.config"), - TEXT("../DotNET/AutoReporter.XmlSerializers.dll"), + TEXT("../Win32/UnrealLightmass.pdb") }; const int32 OptionalDependencyPaths32Count = ARRAY_COUNT(OptionalDependencyPaths32); const TCHAR* OptionalDependencyPaths64[] = { - TEXT("../Win64/UnrealLightmass.pdb"), - TEXT("../DotNET/AutoReporter.exe"), - TEXT("../DotNET/AutoReporter.exe.config"), - TEXT("../DotNET/AutoReporter.XmlSerializers.dll"), + TEXT("../Win64/UnrealLightmass.pdb") }; const int32 OptionalDependencyPaths64Count = ARRAY_COUNT(OptionalDependencyPaths64); diff --git a/Engine/Source/Editor/UnrealEd/Private/PhysicsAssetUtils.cpp b/Engine/Source/Editor/UnrealEd/Private/PhysicsAssetUtils.cpp index 79188d701549..74d8e06f097b 100644 --- a/Engine/Source/Editor/UnrealEd/Private/PhysicsAssetUtils.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/PhysicsAssetUtils.cpp @@ -219,7 +219,7 @@ bool CreateFromSkeletalMeshInternal(UPhysicsAsset* PhysicsAsset, USkeletalMesh* SlowTask.EnterProgressFrame(1.0f, FText::Format(NSLOCTEXT("PhysicsAssetEditor", "ResetCollsionStepInfo", "Generating collision for {0}"), FText::FromName(BoneName))); - int32 NewBodyIndex = CreateNewBody(PhysicsAsset, BoneName); + int32 NewBodyIndex = CreateNewBody(PhysicsAsset, BoneName, Params); UBodySetup* NewBodySetup = PhysicsAsset->SkeletalBodySetups[NewBodyIndex]; check(NewBodySetup->BoneName == BoneName); @@ -846,7 +846,7 @@ void DestroyConstraint(UPhysicsAsset* PhysAsset, int32 ConstraintIndex) } -int32 CreateNewBody(UPhysicsAsset* PhysAsset, FName InBodyName) +int32 CreateNewBody(UPhysicsAsset* PhysAsset, FName InBodyName, const FPhysAssetCreateParams& Params) { check(PhysAsset); @@ -868,6 +868,13 @@ int32 CreateNewBody(UPhysicsAsset* PhysAsset, FName InBodyName) PhysAsset->UpdateBodySetupIndexMap(); PhysAsset->UpdateBoundsBodiesArray(); + if (Params.bDisableCollisionsByDefault) + { + for (int i = 0; i < PhysAsset->SkeletalBodySetups.Num(); ++i) + { + PhysAsset->DisableCollision(i, BodySetupIndex); + } + } // Return index of new body. return BodySetupIndex; } diff --git a/Engine/Source/Editor/UnrealEd/Private/SColorGradientEditor.cpp b/Engine/Source/Editor/UnrealEd/Private/SColorGradientEditor.cpp index be916eb065fc..39c6ff398d4b 100644 --- a/Engine/Source/Editor/UnrealEd/Private/SColorGradientEditor.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/SColorGradientEditor.cpp @@ -57,16 +57,16 @@ bool FGradientStopMark::IsValid( FCurveOwnerInterface& CurveOwner ) bool FGradientStopMark::IsValidAlphaMark( const TArray& Curves ) const { - const FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + const FRealCurve* AlphaCurve = Curves[3].CurveToEdit; return AlphaCurve->IsKeyHandleValid( AlphaKeyHandle ) && AlphaCurve->GetKeyTime( AlphaKeyHandle ) == Time; } bool FGradientStopMark::IsValidColorMark( const TArray& Curves ) const { - const FRichCurve* RedCurve = Curves[0].CurveToEdit; - const FRichCurve* GreenCurve = Curves[1].CurveToEdit; - const FRichCurve* BlueCurve = Curves[2].CurveToEdit; + const FRealCurve* RedCurve = Curves[0].CurveToEdit; + const FRealCurve* GreenCurve = Curves[1].CurveToEdit; + const FRealCurve* BlueCurve = Curves[2].CurveToEdit; return RedCurve->IsKeyHandleValid( RedKeyHandle ) && RedCurve->GetKeyTime( RedKeyHandle ) == Time && GreenCurve->IsKeyHandleValid( GreenKeyHandle ) && GreenCurve->GetKeyTime( GreenKeyHandle ) == Time && @@ -86,9 +86,9 @@ void FGradientStopMark::SetColor( const FLinearColor& InColor, FCurveOwnerInterf // Update the color component on each curve if( IsValidColorMark(Curves) ) { - FRichCurve* RedCurve = Curves[0].CurveToEdit; - FRichCurve* GreenCurve = Curves[1].CurveToEdit; - FRichCurve* BlueCurve = Curves[2].CurveToEdit; + FRealCurve* RedCurve = Curves[0].CurveToEdit; + FRealCurve* GreenCurve = Curves[1].CurveToEdit; + FRealCurve* BlueCurve = Curves[2].CurveToEdit; RedCurve->SetKeyValue( RedKeyHandle, InColor.R ); GreenCurve->SetKeyValue( GreenKeyHandle, InColor.G ); @@ -96,7 +96,7 @@ void FGradientStopMark::SetColor( const FLinearColor& InColor, FCurveOwnerInterf } else if( IsValidAlphaMark(Curves) ) { - FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + FRealCurve* AlphaCurve = Curves[3].CurveToEdit; AlphaCurve->SetKeyValue( AlphaKeyHandle, InColor.A ); } @@ -109,19 +109,19 @@ void FGradientStopMark::SetTime( float NewTime, FCurveOwnerInterface& CurveOwner // Update the time on each curve if( IsValidColorMark(Curves) ) { - FRichCurve* RedCurve = Curves[0].CurveToEdit; - FRichCurve* GreenCurve = Curves[1].CurveToEdit; - FRichCurve* BlueCurve = Curves[2].CurveToEdit; + FRealCurve* RedCurve = Curves[0].CurveToEdit; + FRealCurve* GreenCurve = Curves[1].CurveToEdit; + FRealCurve* BlueCurve = Curves[2].CurveToEdit; - RedKeyHandle = RedCurve->SetKeyTime( RedKeyHandle, NewTime ); - GreenKeyHandle = GreenCurve->SetKeyTime( GreenKeyHandle, NewTime ); - BlueKeyHandle = BlueCurve->SetKeyTime( BlueKeyHandle, NewTime ); + RedCurve->SetKeyTime( RedKeyHandle, NewTime ); + GreenCurve->SetKeyTime( GreenKeyHandle, NewTime ); + BlueCurve->SetKeyTime( BlueKeyHandle, NewTime ); } else if( IsValidAlphaMark(Curves) ) { - FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + FRealCurve* AlphaCurve = Curves[3].CurveToEdit; - AlphaKeyHandle = AlphaCurve->SetKeyTime( AlphaKeyHandle, NewTime ); + AlphaCurve->SetKeyTime( AlphaKeyHandle, NewTime ); } Time = NewTime; @@ -863,16 +863,16 @@ void SColorGradientEditor::GetGradientStopMarks( TArray& OutC // Find Gradient stops // Assume indices 0,1,2 hold R,G,B; - const FRichCurve* RedCurve = Curves[0].CurveToEdit; - const FRichCurve* GreenCurve = Curves[1].CurveToEdit; - const FRichCurve* BlueCurve = Curves[2].CurveToEdit; - const FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + const FRealCurve* RedCurve = Curves[0].CurveToEdit; + const FRealCurve* GreenCurve = Curves[1].CurveToEdit; + const FRealCurve* BlueCurve = Curves[2].CurveToEdit; + const FRealCurve* AlphaCurve = Curves[3].CurveToEdit; // Use the red curve to check the other color channels for keys at the same time for( auto It = RedCurve->GetKeyHandleIterator(); It; ++It ) { - FKeyHandle RedKeyHandle = It.Key(); + FKeyHandle RedKeyHandle = *It; float Time = RedCurve->GetKeyTime( RedKeyHandle ); FKeyHandle GreenKeyHandle = GreenCurve->FindKey( Time ); @@ -891,7 +891,7 @@ void SColorGradientEditor::GetGradientStopMarks( TArray& OutC // Add an alpha gradient stop mark for each alpha key for( auto It = AlphaCurve->GetKeyHandleIterator(); It; ++It ) { - FKeyHandle KeyHandle = It.Key(); + FKeyHandle KeyHandle = *It; float Time = AlphaCurve->GetKeyTime( KeyHandle ); OutAlphaMarks.Add( FGradientStopMark( Time, FKeyHandle(), FKeyHandle(), FKeyHandle(), KeyHandle ) ); @@ -905,10 +905,10 @@ void SColorGradientEditor::DeleteStop( const FGradientStopMark& InMark ) TArray Curves = CurveOwner->GetCurves(); - FRichCurve* RedCurve = Curves[0].CurveToEdit; - FRichCurve* GreenCurve = Curves[1].CurveToEdit; - FRichCurve* BlueCurve = Curves[2].CurveToEdit; - FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + FRealCurve* RedCurve = Curves[0].CurveToEdit; + FRealCurve* GreenCurve = Curves[1].CurveToEdit; + FRealCurve* BlueCurve = Curves[2].CurveToEdit; + FRealCurve* AlphaCurve = Curves[3].CurveToEdit; if( InMark.IsValidAlphaMark( Curves ) ) { @@ -943,9 +943,9 @@ FGradientStopMark SColorGradientEditor::AddStop( const FVector2D& Position, cons if( bColorStop ) { - FRichCurve* RedCurve = Curves[0].CurveToEdit; - FRichCurve* GreenCurve = Curves[1].CurveToEdit; - FRichCurve* BlueCurve = Curves[2].CurveToEdit; + FRealCurve* RedCurve = Curves[0].CurveToEdit; + FRealCurve* GreenCurve = Curves[1].CurveToEdit; + FRealCurve* BlueCurve = Curves[2].CurveToEdit; NewStop.RedKeyHandle = RedCurve->AddKey( NewStopTime, LastModifiedColor.R ); NewStop.GreenKeyHandle = GreenCurve->AddKey( NewStopTime, LastModifiedColor.G ); @@ -953,7 +953,7 @@ FGradientStopMark SColorGradientEditor::AddStop( const FVector2D& Position, cons } else { - FRichCurve* AlphaCurve = Curves[3].CurveToEdit; + FRealCurve* AlphaCurve = Curves[3].CurveToEdit; NewStop.AlphaKeyHandle = AlphaCurve->AddKey( NewStopTime, LastModifiedColor.A ); } diff --git a/Engine/Source/Editor/UnrealEd/Private/SCurveEditor.cpp b/Engine/Source/Editor/UnrealEd/Private/SCurveEditor.cpp index b489b2d42041..99647089ee16 100644 --- a/Engine/Source/Editor/UnrealEd/Private/SCurveEditor.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/SCurveEditor.cpp @@ -27,6 +27,7 @@ #include "Factories/Factory.h" #include "Factories/CurveFactory.h" #include "Editor.h" +#include "Curves/SimpleCurve.h" #include "CurveEditorCommands.h" #include "CurveEditorSettings.h" #include "ScopedTransaction.h" @@ -158,25 +159,27 @@ void SCurveEditor::Construct(const FArguments& InArgs) Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicAuto, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Auto), - FCanExecuteAction(), + FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Auto)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicUser, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_User), - FCanExecuteAction(), + FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_User)); Commands->MapAction(FCurveEditorCommands::Get().InterpolationCubicBreak, FExecuteAction::CreateSP(this, &SCurveEditor::OnSelectInterpolationMode, RCIM_Cubic, RCTM_Break), - FCanExecuteAction(), + FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves), FIsActionChecked::CreateSP(this, &SCurveEditor::IsInterpolationModeSelected, RCIM_Cubic, RCTM_Break)); // Tangents Commands->MapAction(FCurveEditorCommands::Get().FlattenTangents, - FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, true)); + FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, true), + FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves)); Commands->MapAction(FCurveEditorCommands::Get().StraightenTangents, - FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, false)); + FExecuteAction::CreateSP(this, &SCurveEditor::OnFlattenOrStraightenTangents, false), + FCanExecuteAction::CreateSP(this, &SCurveEditor::HasRichCurves)); // Bake and reduce Commands->MapAction(FCurveEditorCommands::Get().BakeCurve, @@ -883,8 +886,10 @@ void SCurveEditor::PaintCurve(TSharedPtr CurveViewModel, const TArray LinePoints; int32 CurveDrawInterval = 1; - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; - if (Curve->GetNumKeys() < 2) + FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + const float NumKeys = Curve->GetNumKeys(); + + if (NumKeys < 2) { //Not enough point, just draw flat line float Value = Curve->Eval(0.0f); @@ -897,15 +902,24 @@ void SCurveEditor::PaintCurve(TSharedPtr CurveViewModel, const } else { + TArray KeyHandles; + TArray> Key_TimeValuePairs; + KeyHandles.Reserve(NumKeys); + Key_TimeValuePairs.Reserve(NumKeys); + for (auto It = Curve->GetKeyHandleIterator(); It; ++It) + { + const FKeyHandle& KeyHandle = *It; + + KeyHandles.Add(KeyHandle); + Key_TimeValuePairs.Emplace(Curve->GetKeyTimeValuePair(KeyHandle)); + } + //Add arrive and exit lines { - const FRichCurveKey& FirstKey = Curve->GetFirstKey(); - const FRichCurveKey& LastKey = Curve->GetLastKey(); - - float ArriveX = ScaleInfo.InputToLocalX(FirstKey.Time); - float ArriveY = ScaleInfo.OutputToLocalY(FirstKey.Value); - float LeaveY = ScaleInfo.OutputToLocalY(LastKey.Value); - float LeaveX = ScaleInfo.InputToLocalX(LastKey.Time); + float ArriveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs[0].Key); + float ArriveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs[0].Value); + float LeaveY = ScaleInfo.OutputToLocalY(Key_TimeValuePairs.Last().Value); + float LeaveX = ScaleInfo.InputToLocalX(Key_TimeValuePairs.Last().Key); //Arrival line LinePoints.Add(FVector2D(0.0f, ArriveY)); @@ -922,10 +936,9 @@ void SCurveEditor::PaintCurve(TSharedPtr CurveViewModel, const //Add enclosed segments - TArray Keys = Curve->GetCopyOfKeys(); - for(int32 i = 0;iGetKeyInterpMode(KeyHandles[i]), Key_TimeValuePairs[i], Key_TimeValuePairs[i+1], LinePoints, ScaleInfo); FSlateDrawElement::MakeLines(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), LinePoints, DrawEffects, Color); LinePoints.Empty(); } @@ -935,37 +948,37 @@ void SCurveEditor::PaintCurve(TSharedPtr CurveViewModel, const } -void SCurveEditor::CreateLinesForSegment( FRichCurve* Curve, const FRichCurveKey& Key1, const FRichCurveKey& Key2, TArray& Points, FTrackScaleInfo &ScaleInfo ) const +void SCurveEditor::CreateLinesForSegment( FRealCurve* Curve, ERichCurveInterpMode InterpMode, const TPair& Key1_TimeValue, const TPair& Key2_TimeValue, TArray& Points, FTrackScaleInfo &ScaleInfo ) const { - switch(Key1.InterpMode) + switch(InterpMode) { case RCIM_Constant: { //@todo: should really only need 3 points here but something about the line rendering isn't quite behaving as I'd expect, so need extras - Points.Add(FVector2D(Key1.Time, Key1.Value)); - Points.Add(FVector2D(Key2.Time, Key1.Value)); - Points.Add(FVector2D(Key2.Time, Key1.Value)); - Points.Add(FVector2D(Key2.Time, Key2.Value)); - Points.Add(FVector2D(Key2.Time, Key1.Value)); + Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key, Key1_TimeValue.Value)); }break; case RCIM_Linear: { - Points.Add(FVector2D(Key1.Time, Key1.Value)); - Points.Add(FVector2D(Key2.Time, Key2.Value)); + Points.Add(FVector2D(Key1_TimeValue.Key, Key1_TimeValue.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key, Key2_TimeValue.Value)); }break; case RCIM_Cubic: { const float StepSize = 1.0f; //clamp to screen to avoid massive slowdown when zoomed in - float StartX = FMath::Max(ScaleInfo.InputToLocalX(Key1.Time), 0.0f) ; - float EndX = FMath::Min(ScaleInfo.InputToLocalX(Key2.Time),ScaleInfo.WidgetSize.X); + float StartX = FMath::Max(ScaleInfo.InputToLocalX(Key1_TimeValue.Key), 0.0f) ; + float EndX = FMath::Min(ScaleInfo.InputToLocalX(Key2_TimeValue.Key),ScaleInfo.WidgetSize.X); for(;StartXEval(CurveIn); Points.Add(FVector2D(CurveIn,CurveOut)); } - Points.Add(FVector2D(Key2.Time,Key2.Value)); + Points.Add(FVector2D(Key2_TimeValue.Key,Key2_TimeValue.Value)); }break; } @@ -984,12 +997,14 @@ void SCurveEditor::PaintKeys(TSharedPtr CurveViewModel, FTrackS { FLinearColor KeyColor = CurveViewModel->bIsLocked ? FLinearColor(0.1f,0.1f,0.1f,1.f) : InWidgetStyle.GetColorAndOpacityTint(); + const bool bHasRichCurves = CurveOwner->HasRichCurves(); + // Iterate over each key ERichCurveInterpMode LastInterpMode = RCIM_Linear; - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { - FKeyHandle KeyHandle = It.Key(); + FKeyHandle KeyHandle = *It; // Work out where it is FVector2D KeyLocation( @@ -1018,9 +1033,13 @@ void SCurveEditor::PaintKeys(TSharedPtr CurveViewModel, FTrackS bool bIsTangentSelected = false; bool bIsArrivalSelected = false; bool bIsLeaveSelected = false; - if(IsTangentVisible(Curve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected) && (Curve->GetKeyInterpMode(KeyHandle) == RCIM_Cubic || LastInterpMode == RCIM_Cubic)) + if (bHasRichCurves) { - PaintTangent(CurveViewModel, ScaleInfo, Curve, KeyHandle, KeyLocation, OutDrawElements, LayerId, AllottedGeometry, MyCullingRect, DrawEffects, LayerToUse, InWidgetStyle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected, bAnyCurveViewModelsSelected); + FRichCurve* RichCurve = (FRichCurve*)Curve; + if (IsTangentVisible(RichCurve, KeyHandle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected) && (RichCurve->GetKeyInterpMode(KeyHandle) == RCIM_Cubic || LastInterpMode == RCIM_Cubic)) + { + PaintTangent(CurveViewModel, ScaleInfo, RichCurve, KeyHandle, KeyLocation, OutDrawElements, LayerId, AllottedGeometry, MyCullingRect, DrawEffects, LayerToUse, InWidgetStyle, bIsTangentSelected, bIsArrivalSelected, bIsLeaveSelected, bAnyCurveViewModelsSelected); + } } LastInterpMode = Curve->GetKeyInterpMode(KeyHandle); @@ -1269,11 +1288,9 @@ void SCurveEditor::SetCurveOwner(FCurveOwnerInterface* InCurveOwner, bool bCanEd CurveViewModels.Empty(); if(CurveOwner != NULL) { - int curveIndex = 0; - for (auto CurveInfo : CurveOwner->GetCurves()) + for (const FRichCurveEditInfo& CurveInfo : CurveOwner->GetCurves()) { CurveViewModels.Add(TSharedPtr(new FCurveViewModel(CurveInfo, CurveOwner->GetCurveColor(CurveInfo), !bCanEdit))); - curveIndex++; } if (bCanEdit) { @@ -1325,7 +1342,7 @@ FCurveOwnerInterface* SCurveEditor::GetCurveOwner() const return CurveOwner; } -FRichCurve* SCurveEditor::GetCurve(int32 CurveIndex) const +FRealCurve* SCurveEditor::GetCurve(int32 CurveIndex) const { if(CurveIndex < CurveViewModels.Num()) { @@ -1338,7 +1355,7 @@ void SCurveEditor::DeleteSelectedKeys() { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_RemoveKeys", "Delete Key(s)")); CurveOwner->ModifyOwner(); - TSet ChangedCurves; + TSet ChangedCurves; // While there are still keys while(SelectedKeys.Num() > 0) @@ -1403,7 +1420,7 @@ void SCurveEditor::AddNewKey(FGeometry InMyGeometry, FVector2D ScreenPosition, T { if (!CurveViewModel->bIsLocked) { - FRichCurve* SelectedCurve = CurveViewModel->CurveInfo.CurveToEdit; + FRealCurve* SelectedCurve = CurveViewModel->CurveInfo.CurveToEdit; if (IsValidCurve(SelectedCurve)) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); @@ -1487,7 +1504,7 @@ FReply SCurveEditor::OnMouseMove( const FGeometry& InMyGeometry, const FPointerE { UpdateCurveToolTip(InMyGeometry, InMouseEvent); - FRichCurve* Curve = GetCurve(0); + FRealCurve* Curve = GetCurve(0); if( Curve != NULL && this->HasMouseCapture()) { if (DragState == EDragState::PreDrag) @@ -1700,7 +1717,7 @@ void SCurveEditor::TryStartDrag(const FGeometry& InMyGeometry, const FPointerEve PreDragTangents.Empty(); for (auto SelectedTangent : SelectedTangents) { - FRichCurve* Curve = SelectedTangent.Key.Curve; + FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve; FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle; float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent; @@ -1729,7 +1746,7 @@ void SCurveEditor::TryStartDrag(const FGeometry& InMyGeometry, const FPointerEve PreDragTangents.Empty(); for (auto SelectedTangent : SelectedTangents) { - FRichCurve* Curve = SelectedTangent.Key.Curve; + FRichCurve* Curve = (FRichCurve*)SelectedTangent.Key.Curve; FKeyHandle KeyHandle = SelectedTangent.Key.KeyHandle; float ArriveTangent = Curve->GetKey(KeyHandle).ArriveTangent; @@ -2155,7 +2172,7 @@ void SCurveEditor::UpdateCurveTimeSingleKey(FSelectedCurveKey Key, int32 NewFram UpdateCurveTimeSingleKey(Key, FrameToTime(NewFrame), true); } -void SCurveEditor::LogAndToastCurveTimeWarning(FRichCurve* Curve) +void SCurveEditor::LogAndToastCurveTimeWarning(FRealCurve* Curve) { FText Error = FText::Format(LOCTEXT("KeyTimeCollision","A key on {0} could not be moved because there was already a key at that time."), FText::FromName(GetViewModelForCurve(Curve)->CurveInfo.CurveName)); FNotificationInfo Info(Error); @@ -2194,7 +2211,7 @@ void SCurveEditor::OnValueComitted(float NewValue, ETextCommit::Type CommitType) { const FScopedTransaction Transaction( LOCTEXT( "CurveEditor_NewValue", "New Value Entered" ) ); CurveOwner->ModifyOwner(); - TSet ChangedCurves; + TSet ChangedCurves; // Iterate over selected set for ( int32 i=0; i < SelectedKeys.Num(); i++ ) @@ -2227,7 +2244,7 @@ void SCurveEditor::OnValueChanged(float NewValue) if ( bIsUsingSlider ) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_NewValue", "New Value Entered")); - TSet ChangedCurves; + TSet ChangedCurves; // Iterate over selected set for ( int32 i=0; i < SelectedKeys.Num(); i++ ) @@ -2293,20 +2310,20 @@ SCurveEditor::FSelectedCurveKey SCurveEditor::HitTestKeys(const FGeometry& InMyG { if (IsCurveSelectable(CurveViewModel)) { - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if(Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { - float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(It.Key())); - float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(It.Key())); + float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It)); + float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It)); if( HitPosition.X > (KeyScreenX - (0.5f * CONST_KeySize.X)) && HitPosition.X < (KeyScreenX + (0.5f * CONST_KeySize.X)) && HitPosition.Y > (KeyScreenY - (0.5f * CONST_KeySize.Y)) && HitPosition.Y < (KeyScreenY + (0.5f * CONST_KeySize.Y)) ) { - return FSelectedCurveKey(Curve, It.Key()); + return FSelectedCurveKey(Curve, *It); } } } @@ -2325,8 +2342,7 @@ void SCurveEditor::MoveSelectedKeys(FVector2D Delta) CurveOwner->ModifyOwnerChange(); // track all unique curves encountered so their tangents can be updated later - TSet UniqueCurves; - + TSet UniqueCurves; // The total move distance for all keys is the difference between the current snapped location // and the start location of the key which was actually dragged. @@ -2343,7 +2359,7 @@ void SCurveEditor::MoveSelectedKeys(FVector2D Delta) } FKeyHandle OldKeyHandle = OldKey.KeyHandle; - FRichCurve* Curve = OldKey.Curve; + FRealCurve* Curve = OldKey.Curve; FVector2D PreDragLocation = PreDragKeyLocations[OldKeyHandle]; FVector2D NewLocation = PreDragLocation + TotalMoveDistance; @@ -2357,20 +2373,23 @@ void SCurveEditor::MoveSelectedKeys(FVector2D Delta) // Changing the time of a key returns a new handle, so make sure to update existing references. if (MovementAxisLock != EMovementAxisLock::AxisLock_Vertical) { - FKeyHandle KeyHandle = Curve->SetKeyTime(OldKeyHandle, NewLocation.X); - SelectedKeys[i] = FSelectedCurveKey(Curve, KeyHandle); + Curve->SetKeyTime(OldKeyHandle, NewLocation.X); + SelectedKeys[i] = FSelectedCurveKey(Curve, OldKeyHandle); PreDragKeyLocations.Remove(OldKeyHandle); - PreDragKeyLocations.Add(KeyHandle, PreDragLocation); + PreDragKeyLocations.Add(OldKeyHandle, PreDragLocation); } UniqueCurves.Add(Curve); ChangedCurveEditInfos.Add(GetViewModelForCurve(Curve)->CurveInfo); } - // update auto tangents for all curves encountered, once each only - for(TSet::TIterator SetIt(UniqueCurves);SetIt;++SetIt) + if (CurveOwner->HasRichCurves()) { - (*SetIt)->AutoSetTangents(); + // update auto tangents for all curves encountered, once each only + for(TSet::TIterator SetIt(UniqueCurves);SetIt;++SetIt) + { + ((FRichCurve*)(*SetIt))->AutoSetTangents(); + } } if (ChangedCurveEditInfos.Num()) @@ -2514,9 +2533,9 @@ bool SCurveEditor::GetAutoFrame() const return Settings->GetAutoFrameCurveEditor() && GetAllowAutoFrame(); } -TArray SCurveEditor::GetCurvesToFit() const +TArray SCurveEditor::GetCurvesToFit() const { - TArray FitCurves; + TArray FitCurves; for(auto CurveViewModel : CurveViewModels) { @@ -2532,7 +2551,7 @@ TArray SCurveEditor::GetCurvesToFit() const void SCurveEditor::ZoomToFitHorizontal(const bool bZoomToFitAll) { - TArray CurvesToFit = GetCurvesToFit(); + TArray CurvesToFit = GetCurvesToFit(); if(CurveViewModels.Num() > 0) { @@ -2568,7 +2587,7 @@ void SCurveEditor::ZoomToFitHorizontal(const bool bZoomToFitAll) } else { - for (FRichCurve* Curve : CurvesToFit) + for (FRealCurve* Curve : CurvesToFit) { float MinTime, MaxTime; Curve->GetTimeRange(MinTime, MaxTime); @@ -2620,7 +2639,7 @@ void SCurveEditor::SetDefaultOutput(const float MinZoomRange) void SCurveEditor::ZoomToFitVertical(const bool bZoomToFitAll) { - TArray CurvesToFit = GetCurvesToFit(); + TArray CurvesToFit = GetCurvesToFit(); if(CurvesToFit.Num() > 0) { @@ -2656,7 +2675,7 @@ void SCurveEditor::ZoomToFitVertical(const bool bZoomToFitAll) } else { - for (FRichCurve* Curve : CurvesToFit) + for (FRealCurve* Curve : CurvesToFit) { float MinVal, MaxVal; Curve->GetValueRange(MinVal, MaxVal); @@ -2917,7 +2936,7 @@ TSharedPtr SCurveEditor::GetCommands() return Commands; } -bool SCurveEditor::IsValidCurve( FRichCurve* Curve ) const +bool SCurveEditor::IsValidCurve( FRealCurve* Curve ) const { bool bIsValid = false; if(Curve && CurveOwner) @@ -2984,7 +3003,7 @@ void SCurveEditor::ClearSelectedCurveViewModels() } } -void SCurveEditor::SetSelectedCurveViewModel(FRichCurve* CurveToSelect) +void SCurveEditor::SetSelectedCurveViewModel(FRealCurve* CurveToSelect) { TSharedPtr ViewModel = GetViewModelForCurve(CurveToSelect); if (ViewModel.IsValid()) @@ -3013,12 +3032,12 @@ TSharedPtr SCurveEditor::HitTestCurves( const FGeometry& InMyG const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( InMouseEvent.GetScreenSpacePosition() ); - TArray CurvesHit; + TArray CurvesHit; for(auto CurveViewModel : CurveViewModels) { - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if(Curve != NULL) { float Time = ScaleInfo.LocalXToInput(HitPosition.X); @@ -3048,9 +3067,8 @@ SCurveEditor::FSelectedTangent SCurveEditor::HitTestCubicTangents( const FGeomet { FSelectedTangent Tangent; - if( AreCurvesVisible() ) + if( AreCurvesVisible() && CurveOwner->HasRichCurves() ) { - FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); const FVector2D HitPosition = InMyGeometry.AbsoluteToLocal( HitScreenPosition); @@ -3059,12 +3077,12 @@ SCurveEditor::FSelectedTangent SCurveEditor::HitTestCubicTangents( const FGeomet { if (IsCurveSelectable(CurveViewModel)) { - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { - FKeyHandle KeyHandle = It.Key(); + FKeyHandle KeyHandle = *It; FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle); if(SelectedCurveKey.IsValid()) @@ -3113,47 +3131,56 @@ SCurveEditor::FSelectedTangent SCurveEditor::HitTestCubicTangents( const FGeomet void SCurveEditor::OnSelectInterpolationMode(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode) { - if(SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) + const bool bHasRichCurves = CurveOwner->HasRichCurves(); + if((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && (InterpMode != RCIM_Cubic || bHasRichCurves)) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_SetInterpolationMode", "Select Interpolation Mode")); CurveOwner->ModifyOwner(); - TSet ChangedCurves; - for(auto It = SelectedKeys.CreateIterator();It;++It) + if (bHasRichCurves) { - FSelectedCurveKey& Key = *It; - check(IsValidCurve(Key.Curve)); - Key.Curve->SetKeyInterpMode(Key.KeyHandle,InterpMode ); - Key.Curve->SetKeyTangentMode(Key.KeyHandle,TangentMode ); + for (auto It = SelectedKeys.CreateIterator(); It; ++It) + { + FSelectedCurveKey& Key = *It; + FRichCurve* RichCurve = (FRichCurve*)Key.Curve; + check(IsValidCurve(RichCurve)); + RichCurve->SetKeyInterpMode(Key.KeyHandle, InterpMode); + RichCurve->SetKeyTangentMode(Key.KeyHandle, TangentMode); + } + + for (auto It = SelectedTangents.CreateIterator(); It; ++It) + { + FSelectedTangent& Tangent = *It; + FRichCurve* RichCurve = (FRichCurve*)Tangent.Key.Curve; + check(IsValidCurve(RichCurve)); + RichCurve->SetKeyInterpMode(Tangent.Key.KeyHandle, InterpMode); + RichCurve->SetKeyTangentMode(Tangent.Key.KeyHandle, TangentMode); + } } - - for(auto It = SelectedTangents.CreateIterator();It;++It) + else { - FSelectedTangent& Tangent = *It; - check(IsValidCurve(Tangent.Key.Curve)); - Tangent.Key.Curve->SetKeyInterpMode(Tangent.Key.KeyHandle,InterpMode ); - Tangent.Key.Curve->SetKeyTangentMode(Tangent.Key.KeyHandle,TangentMode ); + for (auto It = SelectedKeys.CreateIterator(); It; ++It) + { + FSelectedCurveKey& Key = *It; + FSimpleCurve* SimpleCurve = (FSimpleCurve*)Key.Curve; + check(IsValidCurve(SimpleCurve)); + SimpleCurve->SetKeyInterpMode(InterpMode); + } } TArray ChangedCurveEditInfos; - for (auto CurveViewModel : CurveViewModels) - { - if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit)) - { - ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); - } - } CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } bool SCurveEditor::IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode) { + const bool bHasRichCurves = CurveOwner->HasRichCurves(); if (SelectedKeys.Num() > 0) { for (auto SelectedKey : SelectedKeys) { - if (SelectedKey.Curve->GetKeyInterpMode(SelectedKey.KeyHandle) != InterpMode || SelectedKey.Curve->GetKeyTangentMode(SelectedKey.KeyHandle) != TangentMode) + if (SelectedKey.Curve->GetKeyInterpMode(SelectedKey.KeyHandle) != InterpMode || (bHasRichCurves && ((FRichCurve*)SelectedKey.Curve)->GetKeyTangentMode(SelectedKey.KeyHandle) != TangentMode)) { return false; } @@ -3164,7 +3191,7 @@ bool SCurveEditor::IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, { for (auto SelectedTangent : SelectedTangents) { - if (SelectedTangent.Key.Curve->GetKeyInterpMode(SelectedTangent.Key.KeyHandle) != InterpMode || SelectedTangent.Key.Curve->GetKeyTangentMode(SelectedTangent.Key.KeyHandle) != TangentMode) + if (SelectedTangent.Key.Curve->GetKeyInterpMode(SelectedTangent.Key.KeyHandle) != InterpMode || ((FRichCurve*)SelectedTangent.Key.Curve)->GetKeyTangentMode(SelectedTangent.Key.KeyHandle) != TangentMode) { return false; } @@ -3177,21 +3204,25 @@ bool SCurveEditor::IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, } } +bool SCurveEditor::HasRichCurves() const +{ + return CurveOwner->HasRichCurves(); +} + void SCurveEditor::OnFlattenOrStraightenTangents(bool bFlattenTangents) { - if(SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) + if ((SelectedKeys.Num() > 0 || SelectedTangents.Num() > 0) && CurveOwner->HasRichCurves()) { const FScopedTransaction Transaction(LOCTEXT("CurveEditor_FlattenTangents", "Flatten Tangents")); CurveOwner->ModifyOwner(); - TSet ChangedCurves; - for(auto It = SelectedKeys.CreateIterator();It;++It) + auto OnFlattenOrStraightenTangents_Internal = [this, bFlattenTangents](FSelectedCurveKey& Key) { - FSelectedCurveKey& Key = *It; - check(IsValidCurve(Key.Curve)); + FRichCurve* RichCurve = (FRichCurve*)Key.Curve; + check(IsValidCurve(RichCurve)); - float LeaveTangent = Key.Curve->GetKey(Key.KeyHandle).LeaveTangent; - float ArriveTangent = Key.Curve->GetKey(Key.KeyHandle).ArriveTangent; + float LeaveTangent = RichCurve->GetKey(Key.KeyHandle).LeaveTangent; + float ArriveTangent = RichCurve->GetKey(Key.KeyHandle).ArriveTangent; if (bFlattenTangents) { @@ -3204,51 +3235,26 @@ void SCurveEditor::OnFlattenOrStraightenTangents(bool bFlattenTangents) ArriveTangent = LeaveTangent; } - Key.Curve->GetKey(Key.KeyHandle).LeaveTangent = LeaveTangent; - Key.Curve->GetKey(Key.KeyHandle).ArriveTangent = ArriveTangent; - if (Key.Curve->GetKey(Key.KeyHandle).InterpMode == RCIM_Cubic && - Key.Curve->GetKey(Key.KeyHandle).TangentMode == RCTM_Auto) + RichCurve->GetKey(Key.KeyHandle).LeaveTangent = LeaveTangent; + RichCurve->GetKey(Key.KeyHandle).ArriveTangent = ArriveTangent; + if (RichCurve->GetKey(Key.KeyHandle).InterpMode == RCIM_Cubic && + RichCurve->GetKey(Key.KeyHandle).TangentMode == RCTM_Auto) { - Key.Curve->GetKey(Key.KeyHandle).TangentMode = RCTM_User; + RichCurve->GetKey(Key.KeyHandle).TangentMode = RCTM_User; } + }; + + for(auto It = SelectedKeys.CreateIterator();It;++It) + { + OnFlattenOrStraightenTangents_Internal(*It); } for(auto It = SelectedTangents.CreateIterator();It;++It) { - FSelectedTangent& Tangent = *It; - check(IsValidCurve(Tangent.Key.Curve)); - - float LeaveTangent = Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).LeaveTangent; - float ArriveTangent = Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).ArriveTangent; - - if (bFlattenTangents) - { - LeaveTangent = 0; - ArriveTangent = 0; - } - else - { - LeaveTangent = (LeaveTangent + ArriveTangent) * 0.5f; - ArriveTangent = LeaveTangent; - } - - Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).LeaveTangent = LeaveTangent; - Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).ArriveTangent = ArriveTangent; - if (Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).InterpMode == RCIM_Cubic && - Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).TangentMode == RCTM_Auto) - { - Tangent.Key.Curve->GetKey(Tangent.Key.KeyHandle).TangentMode = RCTM_User; - } + OnFlattenOrStraightenTangents_Internal(It->Key); } TArray ChangedCurveEditInfos; - for (auto CurveViewModel : CurveViewModels) - { - if (ChangedCurves.Contains(CurveViewModel->CurveInfo.CurveToEdit)) - { - ChangedCurveEditInfos.Add(CurveViewModel->CurveInfo); - } - } CurveOwner->OnCurveChanged(ChangedCurveEditInfos); } } @@ -3292,10 +3298,10 @@ void SCurveEditor::OnBakeCurveSampleRateCommitted(const FText& InText, ETextComm TArray ChangedCurveEditInfos; // If keys are selected, bake between them - TMap > CurveRangeMap; + TMap > CurveRangeMap; for (auto SelectedKey : SelectedKeys) { - float SelectedTime = SelectedKey.Curve->GetKey(SelectedKey.KeyHandle).Time; + float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle); if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr) { CurveRangeMap[SelectedKey.Curve].Include(SelectedTime); @@ -3370,10 +3376,10 @@ void SCurveEditor::OnReduceCurveToleranceCommitted(const FText& InText, ETextCom TArray ChangedCurveEditInfos; // If keys are selected, bake between them - TMap > CurveRangeMap; + TMap > CurveRangeMap; for (auto SelectedKey : SelectedKeys) { - float SelectedTime = SelectedKey.Curve->GetKey(SelectedKey.KeyHandle).Time; + float SelectedTime = SelectedKey.Curve->GetKeyTime(SelectedKey.KeyHandle); if (CurveRangeMap.Find(SelectedKey.Curve) != nullptr) { CurveRangeMap[SelectedKey.Curve].Include(SelectedTime); @@ -3527,9 +3533,9 @@ void SCurveEditor::MoveTangents(FTrackScaleInfo& ScaleInfo, FVector2D Delta) TArray ChangedCurveEditInfos; CurveOwner->ModifyOwnerChange(); - for (auto SelectedTangent : SelectedTangents) + for (const FSelectedTangent& SelectedTangent : SelectedTangents) { - auto& RichKey = SelectedTangent.Key.Curve->GetKey(SelectedTangent.Key.KeyHandle); + FRichCurveKey& RichKey = ((FRichCurve*)SelectedTangent.Key.Curve)->GetKey(SelectedTangent.Key.KeyHandle); const FSelectedCurveKey &Key = SelectedTangent.Key; float PreDragArriveTangent = PreDragTangents[SelectedTangent.Key.KeyHandle][0]; @@ -3614,8 +3620,8 @@ void SCurveEditor::MoveTangents(FTrackScaleInfo& ScaleInfo, FVector2D Delta) void SCurveEditor::GetTangentPoints( FTrackScaleInfo &ScaleInfo, const FSelectedCurveKey &Key, FVector2D& Arrive, FVector2D& Leave ) const { - FVector2D ArriveTangentDir = CalcTangentDir( Key.Curve->GetKey(Key.KeyHandle).ArriveTangent); - FVector2D LeaveTangentDir = CalcTangentDir( Key.Curve->GetKey(Key.KeyHandle).LeaveTangent); + FVector2D ArriveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).ArriveTangent); + FVector2D LeaveTangentDir = CalcTangentDir( ((FRichCurve*)Key.Curve)->GetKey(Key.KeyHandle).LeaveTangent); FVector2D KeyPosition( Key.Curve->GetKeyTime(Key.KeyHandle),Key.Curve->GetKeyValue(Key.KeyHandle) ); @@ -3651,20 +3657,20 @@ TArray SCurveEditor::GetEditableKeysWithinMarqu { if (IsCurveSelectable(CurveViewModel)) { - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRealCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { - float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(It.Key())); - float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(It.Key())); + float KeyScreenX = ScaleInfo.InputToLocalX(Curve->GetKeyTime(*It)); + float KeyScreenY = ScaleInfo.OutputToLocalY(Curve->GetKeyValue(*It)); if (KeyScreenX >= (MarqueeTopLeft.X - (0.5f * CONST_KeySize.X)) && KeyScreenX <= (MarqueeBottomRight.X + (0.5f * CONST_KeySize.X)) && KeyScreenY >= (MarqueeTopLeft.Y - (0.5f * CONST_KeySize.Y)) && KeyScreenY <= (MarqueeBottomRight.Y + (0.5f * CONST_KeySize.Y))) { - KeysWithinMarquee.Add(FSelectedCurveKey(Curve, It.Key())); + KeysWithinMarquee.Add(FSelectedCurveKey(Curve, *It)); } } } @@ -3682,19 +3688,19 @@ TArray SCurveEditor::GetEditableTangentsWithinMa MarqueeBox.Max = FVector(MarqueeBottomRight.X, MarqueeBottomRight.Y, 0); TArray TangentsWithinMarquee; - if (AreCurvesVisible()) + if (AreCurvesVisible() && CurveOwner->HasRichCurves()) { FTrackScaleInfo ScaleInfo(ViewMinInput.Get(), ViewMaxInput.Get(), ViewMinOutput.Get(), ViewMaxOutput.Get(), InMyGeometry.GetLocalSize()); for (auto CurveViewModel : CurveViewModels) { if (IsCurveSelectable(CurveViewModel)) { - FRichCurve* Curve = CurveViewModel->CurveInfo.CurveToEdit; + FRichCurve* Curve = (FRichCurve*)CurveViewModel->CurveInfo.CurveToEdit; if (Curve != NULL) { for (auto It(Curve->GetKeyHandleIterator()); It; ++It) { - FKeyHandle KeyHandle = It.Key(); + FKeyHandle KeyHandle = *It; FSelectedCurveKey SelectedCurveKey(Curve, KeyHandle); if(SelectedCurveKey.IsValid()) @@ -3830,7 +3836,7 @@ FVector2D SCurveEditor::SnapLocation(FVector2D InLocation) return InLocation; } -TSharedPtr SCurveEditor::GetViewModelForCurve(FRichCurve* InCurve) +TSharedPtr SCurveEditor::GetViewModelForCurve(FRealCurve* InCurve) { for (auto CurveViewModel : CurveViewModels) { diff --git a/Engine/Source/Editor/UnrealEd/Private/ScriptDisassembler.cpp b/Engine/Source/Editor/UnrealEd/Private/ScriptDisassembler.cpp index d98a528ea99b..8b99724edadd 100644 --- a/Engine/Source/Editor/UnrealEd/Private/ScriptDisassembler.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/ScriptDisassembler.cpp @@ -429,6 +429,27 @@ void FKismetBytecodeDisassembler::ProcessCommon(int32& ScriptIndex, EExprToken O DropIndent(); break; } + case EX_LocalVirtualFunction: + { + FString FunctionName = ReadName(ScriptIndex); + Ar.Logf(TEXT("%s $%X: Local Virtual Script Function named %s"), *Indents, (int32)Opcode, *FunctionName); + + while (SerializeExpr(ScriptIndex) != EX_EndFunctionParms) + { + } + break; + } + case EX_LocalFinalFunction: + { + UStruct* StackNode = ReadPointer(ScriptIndex); + Ar.Logf(TEXT("%s $%X: Local Final Script Function (stack node %s::%s)"), *Indents, (int32)Opcode, StackNode ? *StackNode->GetOuter()->GetName() : TEXT("(null)"), StackNode ? *StackNode->GetName() : TEXT("(null)")); + + while (SerializeExpr( ScriptIndex ) != EX_EndFunctionParms) + { + // Params + } + break; + } case EX_LetMulticastDelegate: { Ar.Logf(TEXT("%s $%X: LetMulticastDelegate (Variable = Expression)"), *Indents, (int32)Opcode); diff --git a/Engine/Source/Editor/UnrealEd/Private/StaticMeshEdit.cpp b/Engine/Source/Editor/UnrealEd/Private/StaticMeshEdit.cpp index 3a6c5f1be2b8..6772616d0168 100644 --- a/Engine/Source/Editor/UnrealEd/Private/StaticMeshEdit.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/StaticMeshEdit.cpp @@ -1069,7 +1069,20 @@ ExistingStaticMeshData* SaveExistingStaticMeshData(UStaticMesh* ExistingMesh, Un ExistingMeshDataPtr->UseMaterialNameSlotWorkflow = IsUsingMaterialSlotNameWorkflow(ExistingMesh->AssetImportData); FMeshSectionInfoMap OldSectionInfoMap = ExistingMesh->SectionInfoMap; - + + bool bIsReimportCustomLODOverGeneratedLOD = ExistingMesh->SourceModels.IsValidIndex(LodIndex) && + (ExistingMesh->SourceModels[LodIndex].IsRawMeshEmpty() || !(ExistingMesh->IsReductionActive(LodIndex) && ExistingMesh->SourceModels[LodIndex].ReductionSettings.BaseLODModel != LodIndex)); + + //We need to reset some data in case we import a custom LOD over a generated LOD + if (bIsReimportCustomLODOverGeneratedLOD) + { + //Reset the section info map for this LOD + for (int32 SectionIndex = 0; SectionIndex < ExistingMesh->GetNumSections(LodIndex); ++SectionIndex) + { + OldSectionInfoMap.Remove(LodIndex, SectionIndex); + } + } + ExistingMeshDataPtr->ExistingMaterials.Empty(); if (bSaveMaterials) { @@ -1140,6 +1153,13 @@ ExistingStaticMeshData* SaveExistingStaticMeshData(UStaticMesh* ExistingMesh, Un ExistingMeshDataPtr->ExistingLODData[i].ExistingBuildSettings = ExistingMesh->SourceModels[i].BuildSettings; ExistingMeshDataPtr->ExistingLODData[i].ExistingReductionSettings = ExistingMesh->SourceModels[i].ReductionSettings; + if (bIsReimportCustomLODOverGeneratedLOD && (i == LodIndex)) + { + //Reset the reduction + ExistingMeshDataPtr->ExistingLODData[i].ExistingReductionSettings.PercentTriangles = 1.0f; + ExistingMeshDataPtr->ExistingLODData[i].ExistingReductionSettings.PercentVertices = 1.0f; + ExistingMeshDataPtr->ExistingLODData[i].ExistingReductionSettings.MaxDeviation = 0.0f; + } ExistingMeshDataPtr->ExistingLODData[i].ExistingScreenSize = ExistingMesh->SourceModels[i].ScreenSize.Default; ExistingMeshDataPtr->ExistingLODData[i].ExistingSourceImportFilename = ExistingMesh->SourceModels[i].SourceImportFilename; @@ -1251,7 +1271,6 @@ void RestoreExistingMeshSettings(ExistingStaticMeshData* ExistingMesh, UStaticMe } FMeshDescription* LODMeshDescription = NewMesh->GetMeshDescription(i); bool bSwapFromGeneratedToImported = !ExistingMesh->ExistingLODData[i].ExistingMeshDescription.IsValid() && (LODMeshDescription && LODMeshDescription->Polygons().Num() > 0); - bool bWasReduced = IsReductionActive(ExistingMesh->ExistingLODData[i].ExistingReductionSettings); if (!bSwapFromGeneratedToImported && bWasReduced) diff --git a/Engine/Source/Editor/UnrealEd/Private/TexAlignTools.cpp b/Engine/Source/Editor/UnrealEd/Private/TexAlignTools.cpp index c9f47f2714a8..bbc4ebf51844 100644 --- a/Engine/Source/Editor/UnrealEd/Private/TexAlignTools.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/TexAlignTools.cpp @@ -455,6 +455,8 @@ void UTexAlignerFit::AlignSurf( ETexAlign InTexAlignType, UModel* InModel, FBspS void FTexAlignTools::Init() { + //Never call Init more then once except if Release was call + check(!bIsInit); // Create the list of aligners. Aligners.Empty(); Aligners.Add(NewObject(GetTransientPackage(), NAME_None, RF_Public | RF_Standalone)); @@ -466,17 +468,32 @@ void FTexAlignTools::Init() Aligner->AddToRoot(); } FEditorDelegates::FitTextureToSurface.AddRaw(this, &FTexAlignTools::OnEditorFitTextureToSurface); + bIsInit = true; } +void FTexAlignTools::Release() +{ + if (bIsInit) + { + for (UObject* Aligner : Aligners) + { + Aligner->RemoveFromRoot(); + } + Aligners.Empty(); + FEditorDelegates::FitTextureToSurface.RemoveAll(this); + } + bIsInit = false; +} FTexAlignTools::FTexAlignTools() { + bIsInit = false; } FTexAlignTools::~FTexAlignTools() { - FEditorDelegates::FitTextureToSurface.RemoveAll(this); + Release(); } // Returns the most appropriate texture aligner based on the type passed in. diff --git a/Engine/Source/Editor/UnrealEd/Private/UnrealEdMisc.cpp b/Engine/Source/Editor/UnrealEd/Private/UnrealEdMisc.cpp index d94c2f9f7016..0f417a5eb770 100644 --- a/Engine/Source/Editor/UnrealEd/Private/UnrealEdMisc.cpp +++ b/Engine/Source/Editor/UnrealEd/Private/UnrealEdMisc.cpp @@ -958,6 +958,9 @@ void FUnrealEdMisc::OnExit() } FPlatformProcess::CloseProc(Handle); } + + //Release static class to be sure its not release in a random way. This class use a static multicastdelegate which can be delete before and crash the editor on exit + GTexAlignTools.Release(); } void FUnrealEdMisc::ShutdownAfterError() diff --git a/Engine/Source/Editor/UnrealEd/Public/EdMode.h b/Engine/Source/Editor/UnrealEd/Public/EdMode.h index a1c7eb8f8f24..587ebe2a998f 100644 --- a/Engine/Source/Editor/UnrealEd/Public/EdMode.h +++ b/Engine/Source/Editor/UnrealEd/Public/EdMode.h @@ -80,6 +80,9 @@ public: */ virtual bool CapturedMouseMove( FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY ); + /** Process all captured mouse moves that occurred during the current frame */ + virtual bool ProcessCapturedMouseMoves( FEditorViewportClient* InViewportClient, FViewport* InViewport, const TArrayView& CapturedMouseMoves ) { return false; } + virtual bool InputKey(FEditorViewportClient* ViewportClient,FViewport* Viewport,FKey Key,EInputEvent Event); virtual bool InputAxis(FEditorViewportClient* InViewportClient,FViewport* Viewport,int32 ControllerId,FKey Key,float Delta,float DeltaTime); virtual bool InputDelta(FEditorViewportClient* InViewportClient,FViewport* InViewport,FVector& InDrag,FRotator& InRot,FVector& InScale); diff --git a/Engine/Source/Editor/UnrealEd/Public/Editor.h b/Engine/Source/Editor/UnrealEd/Public/Editor.h index cfba5cbcec78..007773b29ad5 100644 --- a/Engine/Source/Editor/UnrealEd/Public/Editor.h +++ b/Engine/Source/Editor/UnrealEd/Public/Editor.h @@ -112,6 +112,8 @@ struct UNREALED_API FEditorDelegates DECLARE_MULTICAST_DELEGATE(FOnDeleteActorsBegin); /** delegate type for after delete actors is handled */ DECLARE_MULTICAST_DELEGATE(FOnDeleteActorsEnd); + /** delegate type to handle viewing/editing a set of asset identifiers which are packages or ids */ + DECLARE_MULTICAST_DELEGATE_OneParam(FOnViewAssetIdentifiers, TArray); /** Called when the CurrentLevel is switched to a new level. Note that this event won't be fired for temporary changes to the current level, such as when copying/pasting actors. */ @@ -245,6 +247,14 @@ struct UNREALED_API FEditorDelegates static FOnDeleteActorsBegin OnDeleteActorsBegin; /** Sent when delete end called */ static FOnDeleteActorsEnd OnDeleteActorsEnd; + /** Called when you want to view things in the reference viewer, these are bound to by asset manager editor plugins */ + static FOnViewAssetIdentifiers OnOpenReferenceViewer; + /** Called when you want to view things in the size map */ + static FOnViewAssetIdentifiers OnOpenSizeMap; + /** Called when you want to view things in the asset audit window */ + static FOnViewAssetIdentifiers OnOpenAssetAudit; + /** Called to try and edit an asset identifier, which could be a package or searchable name */ + static FOnViewAssetIdentifiers OnEditAssetIdentifiers; }; /** diff --git a/Engine/Source/Editor/UnrealEd/Public/EditorModeManager.h b/Engine/Source/Editor/UnrealEd/Public/EditorModeManager.h index 476099afc39d..b04b93dc18fa 100644 --- a/Engine/Source/Editor/UnrealEd/Public/EditorModeManager.h +++ b/Engine/Source/Editor/UnrealEd/Public/EditorModeManager.h @@ -209,6 +209,9 @@ public: /** Notifies all active modes of captured mouse movement */ bool CapturedMouseMove( FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY ); + /** Notifies all active modes of all captured mouse movement */ + bool ProcessCapturedMouseMoves( FEditorViewportClient* InViewportClient, FViewport* InViewport, const TArrayView& CapturedMouseMoves ); + /** Notifies all active modes of keyboard input */ bool InputKey( FEditorViewportClient* InViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event); diff --git a/Engine/Source/Editor/UnrealEd/Public/EditorReimportHandler.h b/Engine/Source/Editor/UnrealEd/Public/EditorReimportHandler.h index f5b93f68f335..92203eb92717 100644 --- a/Engine/Source/Editor/UnrealEd/Public/EditorReimportHandler.h +++ b/Engine/Source/Editor/UnrealEd/Public/EditorReimportHandler.h @@ -51,10 +51,11 @@ public: * @param bAskForNewFileIfMissing If the file is missing, open a dialog to ask for a new one * @param bShowNotification True to show a notification when complete, false otherwise * @param PreferredReimportFile if not empty, will be use in case the original file is missing and bAskForNewFileIfMissing is set to false + * @param SourceFileIndex which source file index you want to reimport default is INDEX_NONE(the factory will choose) * * @return true if the object was handled by one of the reimport handlers; false otherwise */ - UNREALED_API virtual bool Reimport( UObject* Obj, bool bAskForNewFileIfMissing = false, bool bShowNotification = true, FString PreferredReimportFile = TEXT(""), FReimportHandler* SpecifiedReimportHandler = nullptr ); + UNREALED_API virtual bool Reimport( UObject* Obj, bool bAskForNewFileIfMissing = false, bool bShowNotification = true, FString PreferredReimportFile = TEXT(""), FReimportHandler* SpecifiedReimportHandler = nullptr, int32 SourceFileIndex = INDEX_NONE, bool bForceNewFile = false); /** * Attemp to reimport all specified objects. This function will verify that all source file exist and ask the user @@ -66,8 +67,9 @@ public: * * * @param ToImportObjects Objects to try reimporting * * @param bShowNotification True to show a notification when complete, false otherwise + * * @param SourceFileIndex which source file index you want to reimport default is INDEX_NONE(the factory will chooseP) */ - UNREALED_API virtual void ValidateAllSourceFileAndReimport(TArray &ToImportObjects, bool bShowNotification = true); + UNREALED_API virtual void ValidateAllSourceFileAndReimport(TArray &ToImportObjects, bool bShowNotification = true, int32 SourceFileIndex = INDEX_NONE, bool bForceNewFile = false); /** * Attempt to reimport multiple objects from its source by giving registered reimport @@ -77,10 +79,11 @@ public: * @param bAskForNewFileIfMissing If the file is missing, open a dialog to ask for a new one * @param bShowNotification True to show a notification when complete, false otherwise * @param PreferredReimportFile if not empty, will be use in case the original file is missing and bAskForNewFileIfMissing is set to false + * @param SourceFileIndex which source file index you want to reimport default is INDEX_NONE(the factory will choose) * * @return true if the objects all imported successfully, for more granular success reporting use FReimportManager::Reimport */ - UNREALED_API virtual bool ReimportMultiple( TArrayView Objects, bool bAskForNewFileIfMissing = false, bool bShowNotification = true, FString PreferredReimportFile = TEXT(""), FReimportHandler* SpecifiedReimportHandler = nullptr ); + UNREALED_API virtual bool ReimportMultiple( TArrayView Objects, bool bAskForNewFileIfMissing = false, bool bShowNotification = true, FString PreferredReimportFile = TEXT(""), FReimportHandler* SpecifiedReimportHandler = nullptr, int32 SourceFileIndex = INDEX_NONE ); /** * Update the reimport paths for the specified object @@ -90,6 +93,15 @@ public: */ UNREALED_API virtual void UpdateReimportPaths( UObject* Obj, const TArray& InFilenames ); + /** + * Update the reimport paths for the specified object + * + * @param Obj Object to update + * @param Filename The files we want to set to its import paths + * @param SourceFileIndex the source file index to set the filename + */ + UNREALED_API virtual void UpdateReimportPath(UObject* Obj, const FString& Filename, int32 SourceFileIndex); + /** * Gets the delegate that's fired prior to reimporting an asset * @@ -108,7 +120,7 @@ public: FPostReimportNotification& OnPostReimport(){ return PostReimport; } /** Opens a file dialog to request a new reimport path */ - UNREALED_API void GetNewReimportPath(UObject* Obj, TArray& InOutFilenames); + UNREALED_API void GetNewReimportPath(UObject* Obj, TArray& InOutFilenames, int32 SourceFileIndex = INDEX_NONE); /** FGCObject interface */ virtual void AddReferencedObjects( FReferenceCollector& Collector ) override; @@ -180,6 +192,20 @@ public: */ virtual void SetReimportPaths( UObject* Obj, const TArray& NewReimportPaths ) = 0; + /** + * Sets the reimport path(s) for the specified object at the specified index + * + * @param Obj Object for which to change the reimport path(s) + * @param NewReimportPaths The new path(s) to set on the object + * @param SourceIndex the index of the SourceFile to set the reimport path + */ + virtual void SetReimportPaths(UObject* Obj, const FString& NewReimportPath, const int32 SourceIndex) + { + TArray NewReimportPaths; + NewReimportPaths.Add(NewReimportPath); + SetReimportPaths(Obj, NewReimportPaths); + } + /** * Attempt to reimport the specified object from its source * @@ -191,6 +217,21 @@ public: */ virtual EReimportResult::Type Reimport( UObject* Obj ) = 0; + /** + * Attempt to reimport the specified object from its source + * + * @param Obj Object to attempt to reimport + * @param SourceFileIndex the index of the SourceFile to use to reimport the Obj + * + * @return EReimportResult::Succeeded if this handler was able to handle reimporting the provided object, + * EReimportResult::Failed if this handler was unable to handle reimporting the provided object or + * EReimportResult::Cancelled if the handler was cancelled part-way through re-importing the provided object. + */ + virtual EReimportResult::Type Reimport(UObject* Obj, int32 SourceFileIndex) + { + return Reimport(Obj); + } + /** * Get the import priority for this handler. * Import handlers with higher priority values will take precedent over lower priorities. diff --git a/Engine/Source/Editor/UnrealEd/Public/EditorViewportClient.h b/Engine/Source/Editor/UnrealEd/Public/EditorViewportClient.h index 5882e406acc4..3932a4becc7b 100644 --- a/Engine/Source/Editor/UnrealEd/Public/EditorViewportClient.h +++ b/Engine/Source/Editor/UnrealEd/Public/EditorViewportClient.h @@ -474,6 +474,7 @@ public: virtual void MouseLeave( FViewport* Viewport ) override; virtual EMouseCursor::Type GetCursor(FViewport* Viewport,int32 X,int32 Y) override; virtual void CapturedMouseMove( FViewport* InViewport, int32 InMouseX, int32 InMouseY ) override; + virtual void ProcessAccumulatedPointerInput(FViewport* InViewport) override; virtual bool IsOrtho() const override; virtual void LostFocus(FViewport* Viewport) override; virtual FStatUnitData* GetStatUnitData() const override; @@ -1633,6 +1634,8 @@ private: */ FSceneView* DragStartView; FSceneViewFamily *DragStartViewFamily; + + TArray CapturedMouseMoves; }; diff --git a/Engine/Source/Editor/UnrealEd/Public/FbxImporter.h b/Engine/Source/Editor/UnrealEd/Public/FbxImporter.h index f2def98b04ef..1a134e8bbede 100644 --- a/Engine/Source/Editor/UnrealEd/Public/FbxImporter.h +++ b/Engine/Source/Editor/UnrealEd/Public/FbxImporter.h @@ -1431,7 +1431,7 @@ protected: * @param bDisableMissingBindPoseWarning * @param bUseTime0AsRefPose in/out - Use Time 0 as Ref Pose */ - bool ImportBone(TArray& NodeArray, FSkeletalMeshImportData &ImportData, UFbxSkeletalMeshImportData* TemplateData, TArray &OutSortedLinks, bool& bOutDiffPose, bool bDisableMissingBindPoseWarning, bool & bUseTime0AsRefPose, FbxNode *SkeletalMeshNode); + bool ImportBone(TArray& NodeArray, FSkeletalMeshImportData &ImportData, UFbxSkeletalMeshImportData* TemplateData, TArray &OutSortedLinks, bool& bOutDiffPose, bool bDisableMissingBindPoseWarning, bool & bUseTime0AsRefPose, FbxNode *SkeletalMeshNode, bool bIsReimport); /** * Skins the control points of the given mesh or shape using either the default pose for skinning or the first frame of the diff --git a/Engine/Source/Editor/UnrealEd/Public/LevelEditorViewport.h b/Engine/Source/Editor/UnrealEd/Public/LevelEditorViewport.h index 2f3810896695..1d8b0fc7b4cd 100644 --- a/Engine/Source/Editor/UnrealEd/Public/LevelEditorViewport.h +++ b/Engine/Source/Editor/UnrealEd/Public/LevelEditorViewport.h @@ -685,6 +685,9 @@ private: /** Draw additional details for brushes in the world */ void DrawBrushDetails(const FSceneView* View, FPrimitiveDrawInterface* PDI); + /** Internal function for public FindViewComponentForActor, which finds a view component to use for the specified actor. */ + static USceneComponent* FindViewComponentForActor(AActor const* Actor, TSet& CheckedActors); + public: /** Static: List of objects we're hovering over */ static TSet< FViewportHoverTarget > HoveredObjects; diff --git a/Engine/Source/Editor/UnrealEd/Public/PhysicsAssetUtils.h b/Engine/Source/Editor/UnrealEd/Public/PhysicsAssetUtils.h index 770a70609944..02038bb42f17 100644 --- a/Engine/Source/Editor/UnrealEd/Public/PhysicsAssetUtils.h +++ b/Engine/Source/Editor/UnrealEd/Public/PhysicsAssetUtils.h @@ -46,6 +46,7 @@ struct FPhysAssetCreateParams bCreateConstraints = true; bWalkPastSmall = true; bBodyForAll = false; + bDisableCollisionsByDefault = true; AngularConstraintMode = ACM_Limited; HullCount = 4; MaxHullVerts = 16; @@ -83,6 +84,10 @@ struct FPhysAssetCreateParams UPROPERTY(EditAnywhere, Category = "Body Creation", meta=(DisplayName="Create Body for All Bones")) bool bBodyForAll; + /** Whether to disable collision of body with other bodies on creation */ + UPROPERTY(EditAnywhere, Category = "Body Creation") + bool bDisableCollisionsByDefault; + /** The type of angular constraint to create between bodies */ UPROPERTY(EditAnywhere, Category = "Constraint Creation", meta=(EditCondition="bCreateConstraints")) TEnumAsByte AngularConstraintMode; @@ -174,7 +179,7 @@ namespace FPhysicsAssetUtils * @param InBodyName Name of the new body * @return The Index of the newly created body. */ - UNREALED_API int32 CreateNewBody(UPhysicsAsset* PhysAsset, FName InBodyName); + UNREALED_API int32 CreateNewBody(UPhysicsAsset* PhysAsset, FName InBodyName, const FPhysAssetCreateParams& Params); /** * Destroys the specified body diff --git a/Engine/Source/Editor/UnrealEd/Public/SCurveEditor.h b/Engine/Source/Editor/UnrealEd/Public/SCurveEditor.h index 5113fbc4afa8..9e5d9c2590f3 100644 --- a/Engine/Source/Editor/UnrealEd/Public/SCurveEditor.h +++ b/Engine/Source/Editor/UnrealEd/Public/SCurveEditor.h @@ -275,7 +275,7 @@ public: UNREALED_API bool GetAutoFrame() const; /* Get the curves to will be used during a fit operation */ - UNREALED_API virtual TArray GetCurvesToFit()const; + UNREALED_API TArray GetCurvesToFit() const; /** Zoom to fit */ UNREALED_API void ZoomToFitHorizontal(const bool bZoomToFitAll = false); @@ -291,7 +291,7 @@ private: /** Used to track a key and the curve that owns it */ struct FSelectedCurveKey { - FSelectedCurveKey(FRichCurve* InCurve, FKeyHandle InKeyHandle) + FSelectedCurveKey(FRealCurve* InCurve, FKeyHandle InKeyHandle) : Curve(InCurve), KeyHandle(InKeyHandle) {} @@ -318,7 +318,7 @@ private: return (Curve != Other.Curve) && (KeyHandle != Other.KeyHandle); } - FRichCurve* Curve; + FRealCurve* Curve; FKeyHandle KeyHandle; }; @@ -354,10 +354,10 @@ private: void AddNewKey(FGeometry InMyGeometry, FVector2D ScreenPosition, TSharedPtr>> CurvesToAddKeysTo, bool bAddKeysInline); /** Test if the curve is exists, and if it being displayed on this widget */ - bool IsValidCurve(FRichCurve* Curve) const; + bool IsValidCurve(FRealCurve* Curve) const; /** Util to get a curve by index */ - FRichCurve* GetCurve(int32 CurveIndex) const; + FRealCurve* GetCurve(int32 CurveIndex) const; /** Called when new value for a key is entered */ void NewValueEntered( const FText& NewText, ETextCommit::Type CommitInfo ); @@ -441,7 +441,7 @@ private: void UpdateCurveTimeSingleKey(FSelectedCurveKey Key, float NewTime, bool bSetFromFrame = false); void UpdateCurveTimeSingleKey(FSelectedCurveKey Key, int32 NewFrame); - void LogAndToastCurveTimeWarning(FRichCurve* Curve); + void LogAndToastCurveTimeWarning(FRealCurve* Curve); TOptional OnGetValue() const; void OnValueComitted(float NewValue, ETextCommit::Type CommitType); @@ -533,7 +533,7 @@ private: void ZoomView(FVector2D Delta); /* Generates the line(s) for rendering between KeyIndex and the following key. */ - void CreateLinesForSegment( FRichCurve* Curve, const FRichCurveKey& Key1, const FRichCurveKey& Key2, TArray& Points, FTrackScaleInfo &ScaleInfo) const; + void CreateLinesForSegment( FRealCurve* Curve, ERichCurveInterpMode InterpMode, const TPair& Key1_TimeValue, const TPair& Key2_TimeValue, TArray& Points, FTrackScaleInfo &ScaleInfo) const; /** Detect if user is clicking on a curve */ TSharedPtr HitTestCurves(const FGeometry& InMyGeometry, const FPointerEvent& InMouseEvent); @@ -557,6 +557,8 @@ private: bool IsInterpolationModeSelected(ERichCurveInterpMode InterpMode, ERichCurveTangentMode TangentMode); + bool HasRichCurves() const; + /** Flatten or straighten tangents */ void OnFlattenOrStraightenTangents(bool bFlattenTangents); @@ -617,7 +619,7 @@ private: void UpdateCurveToolTip( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ); - TSharedPtr GetViewModelForCurve(FRichCurve* InCurve); + TSharedPtr GetViewModelForCurve(FRealCurve* InCurve); void OnObjectPropertyChanged(UObject* Object, FPropertyChangedEvent& PropertyChangedEvent); @@ -645,7 +647,7 @@ protected: UNREALED_API void ClearSelectedCurveViewModels(); /** Set the selected curve view model that matches the rich curve */ - UNREALED_API void SetSelectedCurveViewModel(FRichCurve* Curve); + UNREALED_API void SetSelectedCurveViewModel(FRealCurve* Curve); /** Return whether any curve view models are selected */ UNREALED_API bool AnyCurveViewModelsSelected() const; diff --git a/Engine/Source/Editor/UnrealEd/Public/TexAlignTools.h b/Engine/Source/Editor/UnrealEd/Public/TexAlignTools.h index 3b22dcae51e3..82844490021b 100644 --- a/Engine/Source/Editor/UnrealEd/Public/TexAlignTools.h +++ b/Engine/Source/Editor/UnrealEd/Public/TexAlignTools.h @@ -47,6 +47,7 @@ public: */ void Init(); + void Release(); /** * Returns the most appropriate texture aligner based on the type passed in. */ @@ -58,6 +59,11 @@ private: **/ void OnEditorFitTextureToSurface(UWorld* InWorld); + bool bIsInit; + }; +//This structure is using a static multicast delegate, so creating a static instance is dangerous because +//there is nothing to control the destruction order. If the multicast is destroy first we will have dangling pointer. +//The solution to this is to call release in the shutdown of the editor (see FUnrealEdMisc::OnExit) which happen before any static destructor. extern UNREALED_API FTexAlignTools GTexAlignTools; diff --git a/Engine/Source/Programs/AutomationTool/AutomationUtils/DeviceReservation.cs b/Engine/Source/Programs/AutomationTool/AutomationUtils/DeviceReservation.cs index 76ed8e3cead6..28fbf88e1cf7 100644 --- a/Engine/Source/Programs/AutomationTool/AutomationUtils/DeviceReservation.cs +++ b/Engine/Source/Programs/AutomationTool/AutomationUtils/DeviceReservation.cs @@ -237,6 +237,8 @@ namespace AutomationTool.DeviceReservation catch (WebException WebEx) { + Console.WriteLine(String.Format("WebException on reservation request: {0} : {1}", WebEx.Message, WebEx.Status)); + if (RetryCount == RetryMax) { Console.WriteLine("Device reservation unsuccessful"); @@ -318,6 +320,7 @@ namespace AutomationTool.DeviceReservation { Uri BaseUri = new Uri(InBaseUri); Utils.InvokeAPI(BaseUri.AppendPath("api/v1/deviceerror/" + DeviceName), "PUT"); + Console.WriteLine("Reported device problem: {0} : {1}", DeviceName, Error); } catch (Exception Ex) { diff --git a/Engine/Source/Programs/AutomationTool/AutomationUtils/MCPPublic.cs b/Engine/Source/Programs/AutomationTool/AutomationUtils/MCPPublic.cs index d8d64adfe88c..ac242da6c95c 100644 --- a/Engine/Source/Programs/AutomationTool/AutomationUtils/MCPPublic.cs +++ b/Engine/Source/Programs/AutomationTool/AutomationUtils/MCPPublic.cs @@ -1,1758 +1,1787 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using AutomationTool; -using System.Runtime.Serialization; -using System.Net; -using System.Reflection; -using System.Text.RegularExpressions; -using UnrealBuildTool; -using EpicGames.MCP.Automation; - -namespace EpicGames.MCP.Automation -{ - using EpicGames.MCP.Config; - using System.Threading.Tasks; - using Tools.DotNETCommon; - - public static class Extensions - { - public static Type[] SafeGetLoadedTypes(this Assembly Dll) - { - Type[] AllTypes; - try - { - AllTypes = Dll.GetTypes(); - } - catch (ReflectionTypeLoadException e) - { - AllTypes = e.Types.Where(x => x != null).ToArray(); - } - return AllTypes; - } - } - - /// - /// Utility class to provide commit/rollback functionality via an RAII-like functionality. - /// Usage is to provide a rollback action that will be called on Dispose if the Commit() method is not called. - /// This is expected to be used from within a using() ... clause. - /// - public class CommitRollbackTransaction : IDisposable - { - /// - /// Track whether the transaction will be committed. - /// - private bool IsCommitted = false; - - /// - /// - /// - private System.Action RollbackAction; - - /// - /// Call when you want to commit your transaction. Ensures the Rollback action is not called on Dispose(). - /// - public void Commit() - { - IsCommitted = true; - } - - /// - /// Constructor - /// - /// Action to be executed to rollback the transaction. - public CommitRollbackTransaction(System.Action InRollbackAction) - { - RollbackAction = InRollbackAction; - } - - /// - /// Rollback the transaction if its not committed on Dispose. - /// - public void Dispose() - { - if (!IsCommitted) - { - RollbackAction(); - } - } - } - - /// - /// Enum that defines the MCP backend-compatible platform - /// - public enum MCPPlatform - { - /// - /// MCP uses Windows for Win64 - /// - Windows, - - /// - /// 32 bit Windows - /// - Win32, - - /// - /// Mac platform. - /// - Mac, - - /// - /// Linux platform. - /// - Linux, - - /// - /// IOS platform. - /// - IOS, - - /// - /// Android platform. - /// - Android, - - /// - /// WindowsCN Platform. - /// - WindowsCN, - - /// - /// IOSCN Platform. - /// - IOSCN, - - /// - /// AndroidCN Platform. - /// - AndroidCN, - } - - /// - /// Enum that defines CDN types - /// - public enum CDNType - { - /// - /// Internal HTTP CDN server - /// - Internal, - - /// - /// Production HTTP CDN server - /// - Production, - } - - /// - /// Class that holds common state used to control the BuildPatchTool build commands that chunk and create patching manifests and publish build info to the BuildInfoService. - /// - public class BuildPatchToolStagingInfo - { - /// - /// The currently running command, used to get command line overrides - /// - public BuildCommand OwnerCommand; - /// - /// name of the app. Can't always use this to define the staging dir because some apps are not staged to a place that matches their AppName. - /// - public readonly string AppName; - /// - /// Usually the base name of the app. Used to get the MCP key from a branch dictionary. - /// - public readonly string McpConfigKey; - /// - /// ID of the app (needed for the BuildPatchTool) - /// - public readonly int AppID; - /// - /// BuildVersion of the App we are staging. - /// - public readonly string BuildVersion; - /// - /// Metadata for the build consisting of arbitrary json data. Will be null if no metadata exists. - /// - public BuildMetadataBase Metadata; - /// - /// Directory where builds will be staged. Rooted at the BuildRootPath, using a subfolder passed in the ctor, - /// and using BuildVersion/PlatformName to give each builds their own home. - /// - public readonly string StagingDir; - /// - /// Path to the CloudDir where chunks will be written (relative to the BuildRootPath) - /// This is used to copy to the web server, so it can use the same relative path to the root build directory. - /// This allows file to be either copied from the local file system or the webserver using the same relative paths. - /// - public readonly string CloudDirRelativePath; - /// - /// full path to the CloudDir where chunks and manifests should be staged. Rooted at the BuildRootPath, using a subfolder pass in the ctor. - /// - public readonly string CloudDir; - /// - /// Platform we are staging for. - /// - public readonly MCPPlatform Platform; - - /// - /// Gets the base filename of the manifest that would be created by invoking the BuildPatchTool with the given parameters. - /// Note that unless ManifestFilename was provided when constructing this instance, it is strongly recommended to call RetrieveManifestFilename() - /// for existing builds to ensure that the correct filename is used. - /// The legacy behavior of constructing of a manifest filename from appname, buildversion and platform should be considered unreliable, and will emit a warning. - /// - public virtual string ManifestFilename - { - get - { - if (!string.IsNullOrEmpty(_ManifestFilename)) - { - return _ManifestFilename; - } - - CommandUtils.LogInformation("Using legacy behavior of constructing manifest filename from appname, build version and platform. Update your code to specify manifest filename when constructing BuildPatchToolStagingInfo or call RetrieveManifestFilename to query it."); - var BaseFilename = string.Format("{0}{1}-{2}.manifest", - AppName, - BuildVersion, - Platform.ToString()); - return Regex.Replace(BaseFilename, @"\s+", ""); // Strip out whitespace in order to be compatible with BuildPatchTool - } - } - - /// - /// If set, this allows us to over-ride the automatically constructed ManifestFilename - /// - protected string _ManifestFilename; - - /// - /// Determine the platform name - /// - static public MCPPlatform ToMCPPlatform(UnrealTargetPlatform TargetPlatform) - { - if (TargetPlatform == UnrealTargetPlatform.Win64) - { - return MCPPlatform.Windows; - } - else if (TargetPlatform == UnrealTargetPlatform.Win32) - { - return MCPPlatform.Win32; - } - else if (TargetPlatform == UnrealTargetPlatform.Mac) - { - return MCPPlatform.Mac; - } - else if (TargetPlatform == UnrealTargetPlatform.Linux) - { - return MCPPlatform.Linux; - } - - throw new AutomationException("Platform {0} is not properly supported by the MCP backend yet", TargetPlatform); - } - - /// - /// Determine the platform name - /// - static public UnrealTargetPlatform FromMCPPlatform(MCPPlatform TargetPlatform) - { - if (TargetPlatform == MCPPlatform.Windows) - { - return UnrealTargetPlatform.Win64; - } - else if (TargetPlatform == MCPPlatform.Win32) - { - return UnrealTargetPlatform.Win32; - } - else if (TargetPlatform == MCPPlatform.Mac) - { - return UnrealTargetPlatform.Mac; - } - else if (TargetPlatform == MCPPlatform.Linux) - { - return UnrealTargetPlatform.Linux; - } - - throw new AutomationException("Platform {0} is not properly supported by the MCP backend yet", TargetPlatform); - } - - /// - /// Returns the build root path (P:\Builds on build machines usually) - /// - /// - static public string GetBuildRootPath() - { - return CommandUtils.P4Enabled && CommandUtils.AllowSubmit - ? CommandUtils.RootBuildStorageDirectory() - : CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "LocalBuilds"); - } - - /// - /// Basic constructor. - /// - /// - /// - /// - /// - /// Relative path from the BuildRootPath where files will be staged. Commonly matches the AppName. - public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, MCPPlatform platform, string stagingDirRelativePath) - { - OwnerCommand = InOwnerCommand; - AppName = InAppName; - _ManifestFilename = null; - McpConfigKey = InMcpConfigKey; - AppID = InAppID; - BuildVersion = InBuildVersion; - Platform = platform; - string BuildRootPath = GetBuildRootPath(); - StagingDir = CommandUtils.CombinePaths(BuildRootPath, stagingDirRelativePath, BuildVersion, Platform.ToString()); - CloudDirRelativePath = CommandUtils.CombinePaths(stagingDirRelativePath, "CloudDir"); - CloudDir = CommandUtils.CombinePaths(BuildRootPath, CloudDirRelativePath); - Metadata = null; - } - - /// - /// Basic constructor with staging dir suffix override, basically to avoid having platform concatenated - /// - public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, UnrealTargetPlatform InPlatform, string StagingDirRelativePath, string StagingDirSuffix, string InManifestFilename) - : this(InOwnerCommand, InAppName, InMcpConfigKey, InAppID, InBuildVersion, ToMCPPlatform(InPlatform), StagingDirRelativePath, StagingDirSuffix, InManifestFilename) - { - } - - /// - /// Basic constructor with staging dir suffix override, basically to avoid having platform concatenated - /// - /// The automation tool BuildCommand that is currently executing. - /// The name of the app we're working with - /// An identifier for the back-end environment to allow for test deployments, QA, production etc. - /// An identifier for the app. This is deprecated, and can safely be set to zero for all apps. - /// The build version for this build. - /// The platform the build will be deployed to. - /// Relative path from the BuildRootPath where files will be staged. Commonly matches the AppName. - /// By default, we assumed source builds are at build_root/stagingdirrelativepath/buildversion. If they're in a subfolder of this path, specify it here. - /// If specified, will override the value returned by the ManifestFilename property - public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, MCPPlatform InPlatform, string StagingDirRelativePath, string StagingDirSuffix, string InManifestFilename) - { - OwnerCommand = InOwnerCommand; - AppName = InAppName; - McpConfigKey = InMcpConfigKey; - AppID = InAppID; - BuildVersion = InBuildVersion; - Platform = InPlatform; - _ManifestFilename = InManifestFilename; - - string BuildRootPath = GetBuildRootPath(); - StagingDir = CommandUtils.CombinePaths(BuildRootPath, StagingDirRelativePath, BuildVersion, StagingDirSuffix); - CloudDirRelativePath = CommandUtils.CombinePaths(StagingDirRelativePath, "CloudDir"); - CloudDir = CommandUtils.CombinePaths(BuildRootPath, CloudDirRelativePath); - Metadata = null; - } - - /// - /// Constructor which supports being able to just simply call BuildPatchToolBase.Get().Execute - /// - public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, int InAppID, string InBuildVersion, MCPPlatform InPlatform, string InCloudDir) - { - OwnerCommand = InOwnerCommand; - AppName = InAppName; - AppID = InAppID; - BuildVersion = InBuildVersion; - Platform = InPlatform; - CloudDir = InCloudDir; - Metadata = null; - } - - /// - /// Constructor which sets all values directly, without assuming any default paths. - /// - public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, int InAppID, string InBuildVersion, MCPPlatform InPlatform, DirectoryReference InStagingDir, DirectoryReference InCloudDir, string InManifestFilename = null) - { - OwnerCommand = InOwnerCommand; - AppName = InAppName; - AppID = InAppID; - BuildVersion = InBuildVersion; - Platform = InPlatform; - Metadata = null; - if (InStagingDir != null) - { - StagingDir = InStagingDir.FullName; - } - if (InCloudDir != null) - { - DirectoryReference BuildRootDir = new DirectoryReference(GetBuildRootPath()); - if(!InCloudDir.IsUnderDirectory(BuildRootDir)) - { - throw new AutomationException("Cloud directory must be under build root path ({0})", BuildRootDir.FullName); - } - CloudDir = InCloudDir.FullName; - CloudDirRelativePath = InCloudDir.MakeRelativeTo(BuildRootDir).Replace('\\', '/'); - } - if (!string.IsNullOrEmpty(InManifestFilename)) - { - _ManifestFilename = InManifestFilename; - } - } - - /// - /// Associates a piece of metadata (in the form of a key value pair) with the build info. - /// - /// The key to store the metadata against. - /// The value of the metadata to associate. - /// Optional, specifies whether to overwrite the existing key if it exists, default true. - /// The BuildPatchToolStagingInfo to facilitate fluent syntax. - public BuildPatchToolStagingInfo WithMetadata(string Key, string Value, bool bClobber = true) - { - BuildMetadataBase NewMeta = BuildInfoPublisherBase.Get().CreateBuildMetadata(string.Format(@"{{""{0}"":""{1}""}}", Key, Value)); - return WithMetadata(NewMeta, bClobber); - } - - /// - /// Associates new metadata with the build info. - /// - /// The metadata object to merge in. - /// Optional, specifies whether to overwrite the existing key if it exists, default true. - /// The BuildPatchToolStagingInfo to facilitate fluent syntax. - public BuildPatchToolStagingInfo WithMetadata(BuildMetadataBase NewMetadata, bool bClobber = true) - { - if (Metadata != null) - { - Metadata.MergeWith(NewMetadata, bClobber); - } - else - { - Metadata = NewMetadata; - } - return this; - } - - /// - /// Returns the manifest filename, querying build info for it if necessary. - /// If ManifestFilename has already set (either during construction or by a previous call to this method) the cached value will be returned unless bForce is specified. - /// Otherwise, a query will be made to the specified build info service to retrieve the correct manifest filename. - /// - /// Name of which MCP config to check against. - /// If specified, a call will be made to build info even if we have a locally cached version. - /// The manifest filename - public string RetrieveManifestFilename(string McpConfigName, bool bForce = false) - { - if (bForce || string.IsNullOrEmpty(_ManifestFilename)) - { - BuildInfoPublisherBase BI = BuildInfoPublisherBase.Get(); - string ManifestUrl = BI.GetBuildManifestUrl(this, McpConfigName); - if (string.IsNullOrEmpty(ManifestUrl)) - { - throw new AutomationException("Could not determine manifest Url for {0} version {1} from {2} environment.", this.AppName, this.BuildVersion, McpConfigName); - } - _ManifestFilename = ManifestUrl.Split(new char[] { '/', '\\' }).Last(); - } - - return _ManifestFilename; - } - - /// - /// Adds and returns the metadata for this build, querying build info for any additional fields. - /// - /// Optional, name of which MCP config to check against, default null which will use the one stored in this BuildPatchToolStagingInfo. - /// Optional, specifies whether to overwrite existing metadata keys with those received, default false. - /// The Metadata dictionary - public BuildMetadataBase RetrieveBuildMetadata(string McpConfigName = null, bool bClobber = false) - { - BuildInfoPublisherBase BI = BuildInfoPublisherBase.Get(); - return BI.GetBuildMetaData(this, McpConfigName ?? McpConfigKey, bClobber); - } - } - - /// - /// Class that provides programmatic access to the BuildPatchTool - /// - public abstract class BuildPatchToolBase - { - /// - /// Controls which version of BPT to use when executing. - /// - public enum ToolVersion - { - /// - /// The current live, tested build. - /// - Live, - /// - /// The latest published build, may be untested. - /// - Next, - /// - /// An experimental build, for use when one project needs early access or unique changes. - /// - Experimental, - /// - /// Use local build from source of BuildPatchTool. - /// - Source - } - - /// - /// An interface which provides the required Perforce access implementations - /// - public interface IPerforce - { - /// - /// Property to say whether Perforce access is enabled in the environment. - /// - bool bIsEnabled { get; } - - /// - /// Check a file exists in Perforce. - /// - /// Filename to check. - /// True if the file exists in P4. - bool FileExists(string Filename); - - /// - /// Gets the contents of a particular file in the depot and writes it to a local file without syncing it. - /// - /// Depot path to the file (with revision/range if necessary). - /// Output file to write to. - /// True if successful. - bool PrintToFile(string DepotPath, string Filename); - - /// - /// Retrieve the latest CL number for the given file. - /// - /// The filename for the file to check. - /// Receives the CL number. - /// True if the file exists and ChangeList was set. - bool GetLatestChange(string Filename, out int ChangeList); - } - - public class PatchGenerationOptions - { - /// - /// By default, we will only consider data referenced from manifests modified within five days to be reusable. - /// - private const int DEFAULT_DATA_AGE_THRESHOLD = 5; - - public PatchGenerationOptions() - { - DataAgeThreshold = DEFAULT_DATA_AGE_THRESHOLD; - ChunkWindowSize = 1048576; - } - - /// - /// A unique integer for this product. - /// Deprecated. Can be safely left as 0. - /// - public int AppId; - /// - /// The app name for this build, which will be embedded inside the generated manifest to identify the application. - /// - public string AppName; - /// - /// The build version being generated. - /// - public string BuildVersion; - /// - /// Used as part of the build version string. - /// - public MCPPlatform Platform; - /// - /// The directory containing the build image to be read. - /// - public string BuildRoot; - /// - /// The directory which will receive the generated manifest and chunks. - /// - public string CloudDir; - /// - /// The name of the manifest file that will be produced. - /// - public string ManifestFilename; - /// - /// A path to a text file containing BuildRoot relative files to be included in the build. - /// - public string FileInputList; - /// - /// A path to a text file containing BuildRoot relative files to be excluded from the build. - /// - public string FileIgnoreList; - /// - /// A path to a text file containing quoted BuildRoot relative files followed by optional attributes such as readonly compressed executable tag:mytag, separated by \r\n line endings. - /// These attribute will be applied when build is installed client side. - /// - public string FileAttributeList; - /// - /// The path to the app executable, must be relative to, and inside of BuildRoot. - /// - public string AppLaunchCmd; - /// - /// The commandline to send to the app on launch. - /// - public string AppLaunchCmdArgs; - /// - /// The list of prerequisite Ids that this prerequisite installer satisfies. - /// - public List PrereqIds; - /// - /// The prerequisites installer to launch on successful product install, must be relative to, and inside of BuildRoot. - /// - public string PrereqPath; - /// - /// The commandline to send to prerequisites installer on launch. - /// - public string PrereqArgs; - /// - /// When identifying existing patch data to reuse in this build, only - /// files referenced from a manifest file modified within this number of days will be considered for reuse. - /// IMPORTANT: This should always be smaller than the minimum age at which manifest files can be deleted by any cleanup process, to ensure - /// that we do not reuse any files which could be deleted by a concurrently running compactify. It is recommended that this number be at least - /// two days less than the cleanup data age threshold. - /// - public int DataAgeThreshold; - /// - /// Specifies in bytes, the data window size that should be used when saving new chunks. Default is 1048576 (1MiB). - /// - public int ChunkWindowSize; - /// - /// Specifies the desired output FeatureLevel of BuildPatchTool, if this is not provided BPT will warn and default to LatestJson so that project scripts can be updated. - /// - public string FeatureLevel; - /// - /// Contains a list of custom string arguments to be embedded in the generated manifest file. - /// - public List> CustomStringArgs; - /// - /// Contains a list of custom integer arguments to be embedded in the generated manifest file. - /// - public List> CustomIntArgs; - /// - /// Contains a list of custom float arguments to be embedded in the generated manifest file. - /// - public List> CustomFloatArgs; - } - - /// - /// Represents the options passed to the compactify process - /// - public class CompactifyOptions - { - private const int DEFAULT_DATA_AGE_THRESHOLD = 2; - - public CompactifyOptions() - { - DataAgeThreshold = DEFAULT_DATA_AGE_THRESHOLD; - } - - /// - /// BuildPatchTool will run a compactify on this directory. - /// - public string CompactifyDirectory; - /// - /// Corresponds to the -preview parameter - /// - public bool bPreviewCompactify; - /// - /// Patch data files modified within this number of days will *not* be deleted, to ensure that any patch files being written out by a. - /// patch generation process are not deleted before their corresponding manifest file(s) can be written out. - /// NOTE: this should be set to a value larger than the expected maximum time that a build could take. - /// - public int DataAgeThreshold; - } - - public class DataEnumerationOptions - { - /// - /// The file path to the manifest to enumerate from. - /// - public string ManifestFile; - /// - /// The file path to where the list will be saved out, containing \r\n separated cloud relative file paths. - /// - public string OutputFile; - /// - /// When true, the output will include the size of each file on each line, separated by \t - /// - public bool bIncludeSize; - } - - public class ManifestMergeOptions - { - /// - /// The file path to the base manifest. - /// - public string ManifestA; - /// - /// The file path to the update manifest. - /// - public string ManifestB; - /// - /// The file path to the output manifest. - /// - public string ManifestC; - /// - /// The new version string for the build being produced. - /// - public string BuildVersion; - /// - /// Optional. The set of files that should be kept from ManifestA. - /// - public HashSet FilesToKeepFromA; - /// - /// Optional. The set of files that should be kept from ManifestB. - /// - public HashSet FilesToKeepFromB; - } - - public class ManifestDiffOptions - { - /// - /// The file path to the base manifest. - /// - public string ManifestA; - /// - /// The install tags to use for ManifestA. - /// InstallTagsA; - /// - /// The file path to the update manifest. - /// - public string ManifestB; - /// - /// The install tags to use for ManifestB. - /// InstallTagsB; - } - - public class ManifestDiffOutput - { - public class ManifestSummary - { - /// - /// The AppName field from the manifest file. - /// - public string AppName; - /// - /// The AppId field from the manifest file. - /// - public uint AppId; - /// - /// The VersionString field from the manifest file. - /// - public string VersionString; - /// - /// The total size of chunks in the build. - /// - public ulong DownloadSize; - /// - /// The total size of disk space required for the build. - /// - public ulong BuildSize; - /// - /// The list of download sizes for each individual install tag that was used. - /// Note that the sum of these can be higher than the actual total due to possibility of shares files. - /// - public Dictionary IndividualTagDownloadSizes; - /// - /// The list of build sizes for each individual install tag that was used. - /// Note that the sum of these can be higher than the actual total due to possibility of shares files. - /// - public Dictionary IndividualTagBuildSizes; - } - public class ManifestDiff - { - /// - /// The list of build relative paths for files which were added by the patch from ManifestA to ManifestB, subject to using the tags that were provided. - /// - public List NewFilePaths; - /// - /// The list of build relative paths for files which were removed by the patch from ManifestA to ManifestB, subject to using the tags that were provided. - /// - public List RemovedFilePaths; - /// - /// The list of build relative paths for files which were changed between ManifestA and ManifestB, subject to using the tags that were provided. - /// - public List ChangedFilePaths; - /// - /// The list of build relative paths for files which were unchanged between ManifestA and ManifestB, subject to using the tags that were provided. - /// - public List UnchangedFilePaths; - /// - /// The list of cloud directory relative paths for all new chunks required by the patch from ManifestA to ManifestB, subject to using the tags that were provided. - /// - public List NewChunkPaths; - /// - /// The required download size for the patch from ManifestA to ManifestB, subject to using the tags that were provided. - /// - public ulong DeltaDownloadSize; - /// - /// The list of delta sizes for each individual install tag that was used. - /// Note that the sum of these can be higher than the actual total due to possibility of shares files. - /// - public Dictionary IndividualTagDeltaSizes; - } - /// - /// The manifest detail for the source build of the differential. - /// - public ManifestSummary ManifestA; - /// - /// The manifest detail for the target build of the differential. - /// - public ManifestSummary ManifestB; - /// - /// The differential details for the patch from ManifestA's build to ManifestB's build. - /// - public ManifestDiff Differential; - } - - public class AutomationTestsOptions - { - /// - /// Optionally specify the tests to run. - /// - public string TestList; - } - - public class PackageChunksOptions - { - /// - /// Specifies the file path to the manifest to enumerate chunks from. - /// - public string ManifestFile; - /// - /// Specifies the file path to a manifest for a previous build, this will be used to filter out chunks. - /// - public string PrevManifestFile; - /// - /// Specifies the file path to the output package. An extension of .chunkdb will be added if not present. - /// - public string OutputFile; - /// - /// An optional parameter which, if present, specifies the directory where chunks to be packaged can be found. - /// If not specified, the manifest file's location will be used as the cloud directory. - /// - public string CloudDir; - /// - /// Optional value, to restrict the maximum size of each output file (in bytes). - /// If not specified, then only one output file will be produced, containing all the data. - /// If specified, then the output files will be generated as Name.part01.chunkdb, Name.part02.chunkdb etc. The part number will have the number of digits - /// required for highest numbered part. - /// - public ulong? MaxOutputFileSize; - /// - /// Optionally provide a list of tagsets to split chunkdb files on. First all data from the tagset at index 0 will be saved, then any extra data needed - /// for tagset at index 1, and so on. Note that this means the chunkdb files produced for tagset at index 1 will not contain some required data for that tagset if - /// the data already got saved out as part of tagset at index 0, and thus the chunkdb files are additive with no dupes. - /// If it is desired that each tagset's chunkdb files contain the duplicate data, then PackageChunks should be executed once per tagset rather than once will all tagsets. - /// An empty string must be included in one of the tagsets to include untagged file data in that tagset. - /// Leaving this variable null will include data for all files. - /// - public List> TagSetSplit; - } - - public class PackageChunksOutput - { - /// - /// The list of full filepaths of all created chunkdb files. - /// - public List ChunkDbFilePaths; - /// - /// If PackageChunksOptions.TagSetSplit was provided, then this variable will contain a lookup table of TagSetSplit index to List of ChunkDbFilePaths indices. - /// e.g. - /// TagSetLookupTable[0] = [ 0, 1, 2, 3, ..., n ] - /// TagSetLookupTable[1] = [] an empty List would mean that all data for this tagset was already included in the chunkdb(s) for previous tagset(s). - /// TagSetLookupTable[2] = [ n+1, n+2, ..., n+m ] - /// - public List> TagSetLookupTable; - } - - static BuildPatchToolBase Handler = null; - - public static BuildPatchToolBase Get() - { - if (Handler == null) - { - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.SafeGetLoadedTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(BuildPatchToolBase) && typeof(BuildPatchToolBase).IsAssignableFrom(PotentialConfigType)) - { - Handler = Activator.CreateInstance(PotentialConfigType) as BuildPatchToolBase; - break; - } - } - } - if (Handler == null) - { - throw new AutomationException("Attempt to use BuildPatchToolBase.Get() and it doesn't appear that there are any modules that implement this class."); - } - } - return Handler; - } - - /// - /// Runs the Build Patch Tool executable to generate patch data using the supplied parameters. - /// - /// Parameters which will be passed to the patch tool generation process. - /// Which version of BuildPatchTool is desired. - /// If set to true, will allow an existing manifest file to be overwritten with this execution. Default is false. - public abstract void Execute(PatchGenerationOptions Opts, ToolVersion Version = ToolVersion.Live, bool bAllowManifestClobbering = false); - - /// - /// Runs the Build Patch Tool executable to compactify a cloud directory using the supplied parameters. - /// - /// Parameters which will be passed to the patch tool compactify process. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(CompactifyOptions Opts, ToolVersion Version = ToolVersion.Live); - - /// - /// Runs the Build Patch Tool executable to enumerate patch data files referenced by a manifest using the supplied parameters. - /// - /// Parameters which will be passed to the patch tool enumeration process. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(DataEnumerationOptions Opts, ToolVersion Version = ToolVersion.Live); - - /// - /// Runs the Build Patch Tool executable to merge two manifest files producing a hotfix manifest. - /// - /// Parameters which will be passed to the patch tool manifest merge process. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(ManifestMergeOptions Opts, ToolVersion Version = ToolVersion.Live); - - /// - /// Runs the Build Patch Tool executable to diff two manifest files logging out details. - /// - /// Parameters which will be passed to the patch tool manifest diff process. - /// Will receive the data back for the diff. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(ManifestDiffOptions Opts, out ManifestDiffOutput Output, ToolVersion Version = ToolVersion.Live); - - /// - /// Runs the Build Patch Tool executable to evaluate built in automation testing. - /// - /// Parameters which will be passed to the patch tool automation tests process. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(AutomationTestsOptions Opts, ToolVersion Version = ToolVersion.Live); - - /// - /// Runs the Build Patch Tool executable to create ChunkDB file(s) consisting of multiple chunks to allow installing / patching to a specific build. - /// - /// Parameters which will be passed to the patch tool package chunks process. - /// Will receive the data back for the packaging. - /// Which version of BuildPatchTool is desired. - public abstract void Execute(PackageChunksOptions Opts, out PackageChunksOutput Output, ToolVersion Version = ToolVersion.Live); - } - - - /// - /// Class that provides programmatic access to the metadata field from build info. - /// - public abstract class BuildMetadataBase - { - /// - /// Merges provided metadata object into this one. - /// - /// The other metadata to merge in. - /// Optional, specifies whether to overwrite existing metadata keys with those in Other, default true. - /// this object to facilitate fluent syntax. - abstract public BuildMetadataBase MergeWith(BuildMetadataBase Other, bool bClobber = true); - } - - /// - /// Helper class - /// - public abstract class BuildInfoPublisherBase - { - static BuildInfoPublisherBase Handler = null; - - public static BuildInfoPublisherBase Get() - { - if (Handler == null) - { - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.SafeGetLoadedTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(BuildInfoPublisherBase) && typeof(BuildInfoPublisherBase).IsAssignableFrom(PotentialConfigType)) - { - Handler = Activator.CreateInstance(PotentialConfigType) as BuildInfoPublisherBase; - break; - } - } - } - if (Handler == null) - { - throw new AutomationException("Attempt to use BuildInfoPublisherBase.Get() and it doesn't appear that there are any modules that implement this class."); - } - } - return Handler; - } - - /// - /// Creates a metadata object implementation, initialized with the provided JSON object. - /// - /// JSON object representation to initialize with. - /// new instance of metadata implementation. - abstract public BuildMetadataBase CreateBuildMetadata(string JsonRepresentation); - - /// - /// Determines whether a given build is registered in build info - /// - /// The staging info representing the build to check. - /// Name of which MCP config to check against. - /// true if the build is registered, false otherwise - abstract public bool BuildExists(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); - - /// - /// Given a MCPStagingInfo defining our build info, posts the build to the MCP BuildInfo Service. - /// - /// Staging Info describing the BuildInfo to post. - abstract public void PostBuildInfo(BuildPatchToolStagingInfo stagingInfo); - - /// - /// Given a MCPStagingInfo defining our build info and a MCP config name, posts the build to the requested MCP BuildInfo Service. - /// - /// Staging Info describing the BuildInfo to post. - /// Name of which MCP config to post to. - abstract public void PostBuildInfo(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); - - /// - /// Given a BuildVersion defining our a build, return the labels applied to that build - /// - /// Build version to return labels for. - /// Which BuildInfo backend to get labels from for this promotion attempt. - /// The list of build labels applied. - abstract public List GetBuildLabels(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); - - /// - /// Given a staging info defining our build, return the manifest url for that registered build - /// - /// Staging Info describing the BuildInfo to query. - /// Name of which MCP config to query. - /// The manifest url. - abstract public string GetBuildManifestUrl(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); - - /// - /// Given a staging info defining our build, return the manifest url for that registered build - /// - /// Application name to check the label in - /// Build version to manifest for. - /// Name of which MCP config to query. - /// - abstract public string GetBuildManifestUrl(string AppName, string BuildVersionWithPlatform, string McpConfigName); - - /// - /// Given a staging info defining our build, return the manifest hash for that registered build - /// - /// Staging Info describing the BuildInfo to query. - /// Name of which MCP config to query. - /// Manifest SHA1 hash as a hex string - abstract public string GetBuildManifestHash(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); - - /// - /// Given a staging info defining our build, fetch and apply the metadata for that registered build. - /// - /// Staging Info describing the BuildInfo to query. - /// Name of which MCP config to query. - /// Optional, specifies whether to overwrite existing metadata keys with those received, default false. - /// The metadata object for convenience. - abstract public BuildMetadataBase GetBuildMetaData(BuildPatchToolStagingInfo StagingInfo, string McpConfigName, bool bClobber = false); - - /// - /// Get a label string for the specific Platform requested. - /// - /// Base of label - /// Platform to add to base label. - /// The label string including platform postfix. - abstract public string GetLabelWithPlatform(string DestinationLabel, MCPPlatform Platform); - - /// - /// Get a BuildVersion string with the Platform concatenated on. - /// - /// Base of label - /// Platform to add to base label. - /// The BuildVersion string including platform postfix. - abstract public string GetBuildVersionWithPlatform(BuildPatchToolStagingInfo StagingInfo); - - /// - /// Get the BuildVersion for a build that is labeled under a specific appname. - /// - /// Application name to check the label in - /// Label name to get the build version for - /// Which BuildInfo backend to label the build in. - /// The BuildVersion or null if no build labeled. - abstract public string GetLabeledBuildVersion(string AppName, string LabelName, string McpConfigName); - - /// - /// Apply the requested label to the requested build in the BuildInfo backend for the requested MCP environment - /// - /// Staging info for the build to label. - /// Label, including platform, to apply - /// Which BuildInfo backend to label the build in. - abstract public void LabelBuild(BuildPatchToolStagingInfo StagingInfo, string DestinationLabelWithPlatform, string McpConfigName); - - /// - /// Informs Patcher Service of a new build availability after async labeling is complete - /// (this usually means the build was copied to a public file server before the label could be applied). - /// - /// Parent command - /// Application name that the patcher service will use. - /// BuildVersion string that the patcher service will use. - /// Relative path to the Manifest file relative to the global build root (which is like P:\Builds) - /// Name of the label that we will be setting. - abstract public void BuildPromotionCompleted(BuildPatchToolStagingInfo stagingInfo, string AppName, string BuildVersion, string ManifestRelativePath, string PlatformName, string LabelName); - } - - /// - /// Helpers for using the MCP account service - /// - public abstract class McpAccountServiceBase - { - static McpAccountServiceBase Handler = null; - - public static McpAccountServiceBase Get() - { - if (Handler == null) - { - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.SafeGetLoadedTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(McpAccountServiceBase) && typeof(McpAccountServiceBase).IsAssignableFrom(PotentialConfigType)) - { - Handler = Activator.CreateInstance(PotentialConfigType) as McpAccountServiceBase; - break; - } - } - } - if (Handler == null) - { - throw new AutomationException("Attempt to use McpAccountServiceBase.Get() and it doesn't appear that there are any modules that implement this class."); - } - } - return Handler; - } - - /// - /// Gets an OAuth client token for an environment using the default client id and client secret - /// - /// A descriptor for the environment we want a token for - /// An OAuth client token for the specified environment. - public string GetClientToken(McpConfigData McpConfig) - { - return GetClientToken(McpConfig, McpConfig.ClientId, McpConfig.ClientSecret); - } - - /// - /// Gets an OAuth client token using the default client id and client secret and the environment for the specified staging info - /// - /// The staging info for the build we're working with. This will be used to determine the correct back-end service. - /// - public string GetClientToken(BuildPatchToolStagingInfo StagingInfo) - { - McpConfigData McpConfig = McpConfigMapper.FromStagingInfo(StagingInfo); - return GetClientToken(McpConfig); - } - - /// - /// Gets an OAuth client token for an environment using the specified client id and client secret - /// - /// A descriptor for the environment we want a token for - /// The client id used to obtain the token - /// The client secret used to obtain the token - /// An OAuth client token for the specified environment. - public abstract string GetClientToken(McpConfigData McpConfig, string ClientId, string ClientSecret); - - public abstract string SendWebRequest(WebRequest Upload, string Method, string ContentType, byte[] Data); - } - - /// - /// Helper class to manage files stored in some arbitrary cloud storage system - /// - public abstract class CloudStorageBase - { - private static readonly object LockObj = new object(); - private static Dictionary Handlers = new Dictionary(); - private const string DEFAULT_INSTANCE_NAME = "DefaultInstance"; - - /// - /// Gets the default instance of CloudStorageBase - /// - /// A default instance of CloudStorageBase. The first time each instance is returned, it will require initialization with its Init() method. - public static CloudStorageBase Get() - { - return GetByNameImpl(DEFAULT_INSTANCE_NAME); // Identifier for the default cloud storage - } - - /// - /// Gets an instance of CloudStorageBase. - /// Multiple calls with the same instance name will return the same object. - /// - /// The name of the object to return - /// An instance of CloudStorageBase. The first time each instance is returned, it will require initialization with its Init() method. - public static CloudStorageBase GetByName(string InstanceName) - { - if (InstanceName == DEFAULT_INSTANCE_NAME) - { - CommandUtils.LogWarning("CloudStorageBase.GetByName called with {0}. This will return the same instance as Get().", DEFAULT_INSTANCE_NAME); - } - return GetByNameImpl(InstanceName); - } - - private static CloudStorageBase GetByNameImpl(string InstanceName) - { - CloudStorageBase Result = null; - if (!Handlers.TryGetValue(InstanceName, out Result)) - { - lock (LockObj) - { - if (Handlers.ContainsKey(InstanceName)) - { - Result = Handlers[InstanceName]; - } - else - { - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.SafeGetLoadedTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(CloudStorageBase) && typeof(CloudStorageBase).IsAssignableFrom(PotentialConfigType)) - { - Result = Activator.CreateInstance(PotentialConfigType) as CloudStorageBase; - Handlers.Add(InstanceName, Result); - break; - } - } - } - } - } - if (Result == null) - { - throw new AutomationException("Could not find any modules which provide an implementation of CloudStorageBase."); - } - } - return Result; - } - - /// - /// Initializes the provider. - /// Configuration data to initialize the provider. The exact format of the data is provider specific. It might, for example, contain an API key. - /// - abstract public void Init(Dictionary Config, bool bForce = false); - - /// - /// Retrieves a file from the cloud storage provider - /// - /// The name of the folder or container from which contains the file being checked. - /// The identifier or filename of the file to check. - /// If set to true, all log output for the operation is suppressed. - /// True if the file exists in cloud storage, false otherwise. - abstract public bool FileExists(string Container, string Identifier, bool bQuiet = false); - - /// - /// Retrieves a file from the cloud storage provider and saves it to disk. - /// - /// The name of the folder or container from which to retrieve the file. - /// The identifier or filename of the file to retrieve. - /// The full path to the name of the file to save - /// An OUTPUT parameter containing the content's type (null if the cloud provider does not provide this information) - /// If false, and the OutputFile already exists, an error will be thrown. - /// The number of bytes downloaded. - abstract public long DownloadFile(string Container, string Identifier, string OutputFile, out string ContentType, bool bOverwrite = false); - - /// - /// Retrieves a file from the cloud storage provider. - /// - /// The name of the folder or container from which to retrieve the file. - /// The identifier or filename of the file to retrieve. - /// An OUTPUT parameter containing the content's type (null if the cloud provider does not provide this information) - /// A byte array containing the file's contents. - abstract public byte[] GetFile(string Container, string Identifier, out string ContentType); - - /// - /// Posts a file to the cloud storage provider. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// A byte array containing the data to write. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - public PostFileResult PostFile(string Container, string Identifier, byte[] Contents, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false) - { - return PostFileAsync(Container, Identifier, Contents, ContentType, bOverwrite, bMakePublic, bQuiet).Result; - } - - /// - /// Posts a file to the cloud storage provider asynchronously. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// A byte array containing the data to write. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - abstract public Task PostFileAsync(string Container, string Identifier, byte[] Contents, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false); - - /// - /// Posts a file to the cloud storage provider. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// The full path of the file to upload. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - public PostFileResult PostFile(string Container, string Identifier, string SourceFilePath, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false) - { - return PostFileAsync(Container, Identifier, SourceFilePath, ContentType, bOverwrite, bMakePublic, bQuiet).Result; - } - - /// - /// Posts a file to the cloud storage provider asynchronously. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// The full path of the file to upload. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - abstract public Task PostFileAsync(string Container, string Identifier, string SourceFilePath, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false); - - /// - /// Posts a file to the cloud storage provider using multiple connections. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// The full path of the file to upload. - /// The number of concurrent connections to use during uploading. - /// The size of each part that is uploaded. Minimum (and default) is 5 MB. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - public PostFileResult PostMultipartFile(string Container, string Identifier, string SourceFilePath, int NumConcurrentConnections, decimal PartSizeMegabytes = 5.0m, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false) - { - return PostMultipartFileAsync(Container, Identifier, SourceFilePath, NumConcurrentConnections, PartSizeMegabytes, ContentType, bOverwrite, bMakePublic, bQuiet).Result; - } - - /// - /// Posts a file to the cloud storage provider using multiple connections asynchronously. - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - /// The full path of the file to upload. - /// The number of concurrent connections to use during uploading. - /// The size of each part that is uploaded. Minimum (and default) is 5 MB. - /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. - /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. - /// Specifies whether the file should be made publicly readable. - /// If set to true, all log output for the operation is supressed. - /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. - abstract public Task PostMultipartFileAsync(string Container, string Identifier, string SourceFilePath, int NumConcurrentConnections, decimal PartSizeMegabytes = 5.0m, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false); - - /// - /// Deletes a file from cloud storage - /// - /// The name of the folder or container in which to store the file. - /// The identifier or filename of the file to write. - abstract public void DeleteFile(string Container, string Identifier); - - /// - /// Deletes a folder from cloud storage - /// - /// The name of the folder or container from which to delete the file. - /// The identifier or name of the folder to delete. - abstract public void DeleteFolder(string Container, string FolderIdentifier); - - /// - /// Retrieves a list of folders from the cloud storage provider - /// - /// The name of the container from which to list folders. - /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. - /// An action which acts upon an options object to configure the operation. See ListOptions for more details. - /// An array of paths to the folders in the specified container and matching the prefix constraint. - public string[] ListFolders(string Container, string Prefix, Action Options) - { - ListOptions Opts = new ListOptions(); - if (Options != null) - { - Options(Opts); - } - return ListFolders(Container, Prefix, Opts); - } - - /// - /// Retrieves a list of folders from the cloud storage provider - /// - /// The name of the container from which to list folders. - /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. - /// An options object to configure the operation. See ListOptions for more details. - /// An array of paths to the folders in the specified container and matching the prefix constraint. - abstract public string[] ListFolders(string Container, string Prefix, ListOptions Options); - - /// - /// DEPRECATED. Retrieves a list of files from the cloud storage provider. See overload with ListOptions for non-deprecated use. - /// - /// The name of the folder or container from which to list files. - /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. - /// Indicates whether the list of files returned should traverse subdirectories - /// If set to true, all log output for the operation is supressed. - /// An array of paths to the files in the specified location and matching the prefix constraint. - public string[] ListFiles(string Container, string Prefix = null, bool bRecursive = true, bool bQuiet = false) - { - return ListFiles(Container, Prefix, opts => - { - opts.bRecursive = bRecursive; - opts.bQuiet = bQuiet; - }); - } - - /// - /// Retrieves a list of files from the cloud storage provider - /// - /// The name of the container from which to list folders. - /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. - /// An action which acts upon an options object to configure the operation. See ListOptions for more details. - /// An array of paths to the folders in the specified container and matching the prefix constraint. - public string[] ListFiles(string Container, string Prefix, Action Options) - { - ListOptions Opts = new ListOptions(); - if (Options != null) - { - Options(Opts); - } - return ListFiles(Container, Prefix, Opts); - } - - /// - /// Retrieves a list of files from the cloud storage provider. - /// - /// The name of the folder or container from which to list files. - /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. - /// An options object to configure the operation. See ListOptions for more details. - /// An array of paths to the files in the specified location and matching the prefix constraint. - abstract public string[] ListFiles(string Container, string Prefix, ListOptions Options); - - /// - /// Retrieves a list of files together with basic metadata from the cloud storage provider - /// - /// The name of the container from which to list folders. - /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. - /// An action which acts upon an options object to configure the operation. See ListOptions for more details. - /// An array of metadata objects (including filenames) to the files in the specified location and matching the prefix constraint. - public ObjectMetadata[] ListFilesWithMetadata(string Container, string Prefix, Action Options) - { - ListOptions Opts = new ListOptions(); - if (Options != null) - { - Options(Opts); - } - return ListFilesWithMetadata(Container, Prefix, Opts); - } - - /// - /// Retrieves a list of files together with basic metadata from the cloud storage provider. - /// - /// The name of the folder or container from which to list files. - /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. - /// An options object to configure the operation. See ListOptions for more details. - /// An array of metadata objects (including filenames) to the files in the specified location and matching the prefix constraint. - abstract public ObjectMetadata[] ListFilesWithMetadata(string Container, string Prefix, ListOptions Options); - - /// - /// Sets one or more items of metadata on an object in cloud storage - /// - /// The name of the folder or container in which the file is stored. - /// The identifier of filename of the file to set metadata on. - /// A dictionary containing the metadata keys and their values - /// If true, then existing metadata will be replaced (or overwritten if the keys match). If false, no existing metadata is retained. - abstract public void SetMetadata(string Container, string Identifier, IDictionary Metadata, bool bMerge = true); - - /// - /// Gets all items of metadata on an object in cloud storage. Metadata values are all returned as strings. - /// - /// The name of the folder or container in which the file is stored. - /// The identifier of filename of the file to get metadata. - abstract public Dictionary GetMetadata(string Container, string Identifier); - - /// - /// Gets an item of metadata from an object in cloud storage. The object is casted to the specified type. - /// - /// The name of the folder or container in which the file is stored. - /// The identifier of filename of the file to get metadata. - /// The key of the item of metadata to retrieve. - abstract public T GetMetadata(string Container, string Identifier, string MetadataKey); - - /// - /// Updates the timestamp on a particular file in cloud storage to the current time. - /// - /// The name of the container in which the file is stored. - /// The identifier of filename of the file to touch. - abstract public void TouchFile(string Container, string Identifier); - - /// - /// Copies manifest and chunks from a staged location to cloud storage. - /// - /// The name of the container in which to store files. - /// Staging info used to determine where the chunks are to copy. - /// If true, will always copy the manifest and chunks to cloud storage. Otherwise, will only copy if the manifest isn't already present on cloud storage. - /// True if the build was copied to cloud storage, false otherwise. - abstract public bool CopyChunksToCloudStorage(string Container, BuildPatchToolStagingInfo StagingInfo, bool bForce = false); - - /// - /// Copies manifest and its chunks from a specific path to a given target folder in the cloud. - /// - /// The name of the container in which to store files. - /// The path within the container that the files should be stored in. - /// The full path of the manifest file to copy. - /// If true, will always copy the manifest and chunks to cloud storage. Otherwise, will only copy if the manifest isn't already present on cloud storage. - /// True if the build was copied to cloud storage, false otherwise. - abstract public bool CopyChunksToCloudStorage(string Container, string RemoteCloudDir, string ManifestFilePath, bool bForce = false); - - /// - /// Verifies whether a manifest for a given build is in cloud storage. - /// - /// The name of the folder or container in which to store files. - /// Staging info representing the build to check. - /// True if the manifest exists in cloud storage, false otherwise. - abstract public bool IsManifestOnCloudStorage(string Container, BuildPatchToolStagingInfo StagingInfo); - - public class PostFileResult - { - /// - /// Set to the URL of the uploaded file on success - /// - public string ObjectURL { get; set; } - - /// - /// Set to true if the write succeeds, false otherwise. - /// - public bool bSuccess { get; set; } - } - - /// - /// Encapsulates options used when listing files or folders using ListFiles and ListFolders - /// - public class ListOptions - { - public ListOptions() - { - bQuiet = false; - bRecursive = false; - bReturnURLs = true; - } - - /// - /// If set to true, all log output for the operation is suppressed. Defaults to false. - /// - public bool bQuiet { get; set; } - - /// - /// Indicates whether the list of files returned should traverse subfolders. Defaults to false. - /// - public bool bRecursive { get; set; } - - /// - /// If true, returns the full URL to the listed objects. If false, returns their identifier within the container. Defaults to true. - /// - public bool bReturnURLs { get; set; } - } - - public class ObjectMetadata - { - public string ETag { get; set; } - public string Path { get; set; } - public string Url { get; set; } - public DateTime LastModified { get; set; } - public long Size { get; set; } - } - } - - public abstract class CatalogServiceBase - { - static CatalogServiceBase Handler = null; - - public static CatalogServiceBase Get() - { - if (Handler == null) - { - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.GetTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(CatalogServiceBase) && typeof(CatalogServiceBase).IsAssignableFrom(PotentialConfigType)) - { - Handler = Activator.CreateInstance(PotentialConfigType) as CatalogServiceBase; - break; - } - } - } - if (Handler == null) - { - throw new AutomationException("Attempt to use McpCatalogServiceBase.Get() and it doesn't appear that there are any modules that implement this class."); - } - } - return Handler; - } - - public abstract string GetAppName(string ItemId, string[] EngineVersions, string McpConfigName); - public abstract string[] GetNamespaces(string McpConfigName); - public abstract McpCatalogItem GetItemById(string Namespace, string ItemId, string McpConfigName); - public abstract IEnumerable GetAllItems(string Namespace, string McpConfigName); - - public class McpCatalogItem - { - public string Id { get; set; } - public string Title { get; set; } - public string Description { get; set; } - public string LongDescription { get; set; } - public string TechnicalDetails { get; set; } - public string Namespace { get; set; } - public string Status { get; set; } - public DateTime CreationDate { get; set; } - public DateTime LastModifiedDate { get; set; } - public ReleaseInfo[] ReleaseInfo { get; set; } - } - - public class ReleaseInfo - { - public string AppId { get; set; } - public string[] CompatibleApps { get; set; } - public string[] Platform { get; set; } - public DateTime DateAdded { get; set; } - } - } -} - -namespace EpicGames.MCP.Config -{ - /// - /// Class for retrieving MCP configuration data - /// - public class McpConfigHelper - { - // List of configs is cached off for fetching from multiple times - private static Dictionary Configs; - - public static McpConfigData Find(string ConfigName) - { - if (Configs == null) - { - // Load all secret configs by trying to instantiate all classes derived from McpConfig from all loaded DLLs. - // Note that we're using the default constructor on the secret config types. - Configs = new Dictionary(); - Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var Dll in LoadedAssemblies) - { - Type[] AllTypes = Dll.SafeGetLoadedTypes(); - foreach (var PotentialConfigType in AllTypes) - { - if (PotentialConfigType != typeof(McpConfigData) && typeof(McpConfigData).IsAssignableFrom(PotentialConfigType)) - { - try - { - McpConfigData Config = Activator.CreateInstance(PotentialConfigType) as McpConfigData; - if (Config != null) - { - Configs.Add(Config.Name, Config); - } - } - catch - { - BuildCommand.LogWarning("Unable to create McpConfig: {0}", PotentialConfigType.Name); - } - } - } - } - } - McpConfigData LoadedConfig; - Configs.TryGetValue(ConfigName, out LoadedConfig); - if (LoadedConfig == null) - { - throw new AutomationException("Unable to find requested McpConfig: {0}", ConfigName); - } - return LoadedConfig; - } - } - - // Class for storing mcp configuration data - public class McpConfigData - { - public McpConfigData(string InName, string InAccountBaseUrl, string InFortniteBaseUrl, string InLauncherBaseUrl, string InBuildInfoV2BaseUrl, string InLauncherV2BaseUrl, string InCatalogBaseUrl, string InClientId, string InClientSecret) - { - Name = InName; - AccountBaseUrl = InAccountBaseUrl; - FortniteBaseUrl = InFortniteBaseUrl; - LauncherBaseUrl = InLauncherBaseUrl; - BuildInfoV2BaseUrl = InBuildInfoV2BaseUrl; - LauncherV2BaseUrl = InLauncherV2BaseUrl; - CatalogBaseUrl = InCatalogBaseUrl; - ClientId = InClientId; - ClientSecret = InClientSecret; - } - - public string Name; - public string AccountBaseUrl; - public string FortniteBaseUrl; - public string LauncherBaseUrl; - public string BuildInfoV2BaseUrl; - public string LauncherV2BaseUrl; - public string CatalogBaseUrl; - public string ClientId; - public string ClientSecret; - - public void SpewValues() - { - CommandUtils.LogVerbose("Name : {0}", Name); - CommandUtils.LogVerbose("AccountBaseUrl : {0}", AccountBaseUrl); - CommandUtils.LogVerbose("FortniteBaseUrl : {0}", FortniteBaseUrl); - CommandUtils.LogVerbose("LauncherBaseUrl : {0}", LauncherBaseUrl); - CommandUtils.LogVerbose("BuildInfoV2BaseUrl : {0}", BuildInfoV2BaseUrl); - CommandUtils.LogVerbose("LauncherV2BaseUrl : {0}", LauncherV2BaseUrl); - CommandUtils.LogVerbose("CatalogBaseUrl : {0}", CatalogBaseUrl); - CommandUtils.LogVerbose("ClientId : {0}", ClientId); - // we don't really want this in logs CommandUtils.Log("ClientSecret : {0}", ClientSecret); - } - } - - public class McpConfigMapper - { - static public McpConfigData FromMcpConfigKey(string McpConfigKey) - { - return McpConfigHelper.Find("MainGameDevNet"); - } - - static public McpConfigData FromStagingInfo(EpicGames.MCP.Automation.BuildPatchToolStagingInfo StagingInfo) - { - string McpConfigNameToLookup = null; - if (StagingInfo.OwnerCommand != null) - { - McpConfigNameToLookup = StagingInfo.OwnerCommand.ParseParamValue("MCPConfig"); - } - if (String.IsNullOrEmpty(McpConfigNameToLookup)) - { - return FromMcpConfigKey(StagingInfo.McpConfigKey); - } - return McpConfigHelper.Find(McpConfigNameToLookup); - } - } - -} +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using AutomationTool; +using System.Runtime.Serialization; +using System.Net; +using System.Reflection; +using System.Text.RegularExpressions; +using UnrealBuildTool; +using EpicGames.MCP.Automation; + +namespace EpicGames.MCP.Automation +{ + using EpicGames.MCP.Config; + using System.Threading.Tasks; + using Tools.DotNETCommon; + + public static class Extensions + { + public static Type[] SafeGetLoadedTypes(this Assembly Dll) + { + Type[] AllTypes; + try + { + AllTypes = Dll.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + AllTypes = e.Types.Where(x => x != null).ToArray(); + } + return AllTypes; + } + } + + /// + /// Utility class to provide commit/rollback functionality via an RAII-like functionality. + /// Usage is to provide a rollback action that will be called on Dispose if the Commit() method is not called. + /// This is expected to be used from within a using() ... clause. + /// + public class CommitRollbackTransaction : IDisposable + { + /// + /// Track whether the transaction will be committed. + /// + private bool IsCommitted = false; + + /// + /// + /// + private System.Action RollbackAction; + + /// + /// Call when you want to commit your transaction. Ensures the Rollback action is not called on Dispose(). + /// + public void Commit() + { + IsCommitted = true; + } + + /// + /// Constructor + /// + /// Action to be executed to rollback the transaction. + public CommitRollbackTransaction(System.Action InRollbackAction) + { + RollbackAction = InRollbackAction; + } + + /// + /// Rollback the transaction if its not committed on Dispose. + /// + public void Dispose() + { + if (!IsCommitted) + { + RollbackAction(); + } + } + } + + /// + /// Enum that defines the MCP backend-compatible platform + /// + public enum MCPPlatform + { + /// + /// MCP uses Windows for Win64 + /// + Windows, + + /// + /// 32 bit Windows + /// + Win32, + + /// + /// Mac platform. + /// + Mac, + + /// + /// Linux platform. + /// + Linux, + + /// + /// IOS platform. + /// + IOS, + + /// + /// Android platform. + /// + Android, + + /// + /// WindowsCN Platform. + /// + WindowsCN, + + /// + /// IOSCN Platform. + /// + IOSCN, + + /// + /// AndroidCN Platform. + /// + AndroidCN, + } + + /// + /// Enum that defines CDN types + /// + public enum CDNType + { + /// + /// Internal HTTP CDN server + /// + Internal, + + /// + /// Production HTTP CDN server + /// + Production, + } + + /// + /// Class that holds common state used to control the BuildPatchTool build commands that chunk and create patching manifests and publish build info to the BuildInfoService. + /// + public class BuildPatchToolStagingInfo + { + /// + /// The currently running command, used to get command line overrides + /// + public BuildCommand OwnerCommand; + /// + /// name of the app. Can't always use this to define the staging dir because some apps are not staged to a place that matches their AppName. + /// + public readonly string AppName; + /// + /// Usually the base name of the app. Used to get the MCP key from a branch dictionary. + /// + public readonly string McpConfigKey; + /// + /// ID of the app (needed for the BuildPatchTool) + /// + public readonly int AppID; + /// + /// BuildVersion of the App we are staging. + /// + public readonly string BuildVersion; + /// + /// Metadata for the build consisting of arbitrary json data. Will be null if no metadata exists. + /// + public BuildMetadataBase Metadata; + /// + /// Directory where builds will be staged. Rooted at the BuildRootPath, using a subfolder passed in the ctor, + /// and using BuildVersion/PlatformName to give each builds their own home. + /// + public readonly string StagingDir; + /// + /// Path to the CloudDir where chunks will be written (relative to the BuildRootPath) + /// This is used to copy to the web server, so it can use the same relative path to the root build directory. + /// This allows file to be either copied from the local file system or the webserver using the same relative paths. + /// + public readonly string CloudDirRelativePath; + /// + /// full path to the CloudDir where chunks and manifests should be staged. Rooted at the BuildRootPath, using a subfolder pass in the ctor. + /// + public readonly string CloudDir; + /// + /// Platform we are staging for. + /// + public readonly MCPPlatform Platform; + + /// + /// Gets the base filename of the manifest that would be created by invoking the BuildPatchTool with the given parameters. + /// Note that unless ManifestFilename was provided when constructing this instance, it is strongly recommended to call RetrieveManifestFilename() + /// for existing builds to ensure that the correct filename is used. + /// The legacy behavior of constructing of a manifest filename from appname, buildversion and platform should be considered unreliable, and will emit a warning. + /// + public virtual string ManifestFilename + { + get + { + if (!string.IsNullOrEmpty(_ManifestFilename)) + { + return _ManifestFilename; + } + + CommandUtils.LogInformation("Using legacy behavior of constructing manifest filename from appname, build version and platform. Update your code to specify manifest filename when constructing BuildPatchToolStagingInfo or call RetrieveManifestFilename to query it."); + var BaseFilename = string.Format("{0}{1}-{2}.manifest", + AppName, + BuildVersion, + Platform.ToString()); + return Regex.Replace(BaseFilename, @"\s+", ""); // Strip out whitespace in order to be compatible with BuildPatchTool + } + } + + /// + /// If set, this allows us to over-ride the automatically constructed ManifestFilename + /// + protected string _ManifestFilename; + + /// + /// Determine the platform name + /// + static public MCPPlatform ToMCPPlatform(UnrealTargetPlatform TargetPlatform) + { + if (TargetPlatform == UnrealTargetPlatform.Win64) + { + return MCPPlatform.Windows; + } + else if (TargetPlatform == UnrealTargetPlatform.Win32) + { + return MCPPlatform.Win32; + } + else if (TargetPlatform == UnrealTargetPlatform.Mac) + { + return MCPPlatform.Mac; + } + else if (TargetPlatform == UnrealTargetPlatform.Linux) + { + return MCPPlatform.Linux; + } + + throw new AutomationException("Platform {0} is not properly supported by the MCP backend yet", TargetPlatform); + } + + /// + /// Determine the platform name + /// + static public UnrealTargetPlatform FromMCPPlatform(MCPPlatform TargetPlatform) + { + if (TargetPlatform == MCPPlatform.Windows) + { + return UnrealTargetPlatform.Win64; + } + else if (TargetPlatform == MCPPlatform.Win32) + { + return UnrealTargetPlatform.Win32; + } + else if (TargetPlatform == MCPPlatform.Mac) + { + return UnrealTargetPlatform.Mac; + } + else if (TargetPlatform == MCPPlatform.Linux) + { + return UnrealTargetPlatform.Linux; + } + + throw new AutomationException("Platform {0} is not properly supported by the MCP backend yet", TargetPlatform); + } + + /// + /// Returns the build root path (P:\Builds on build machines usually) + /// + /// + static public string GetBuildRootPath() + { + return CommandUtils.P4Enabled && CommandUtils.AllowSubmit + ? CommandUtils.RootBuildStorageDirectory() + : CommandUtils.CombinePaths(CommandUtils.CmdEnv.LocalRoot, "LocalBuilds"); + } + + /// + /// Basic constructor. + /// + /// + /// + /// + /// + /// Relative path from the BuildRootPath where files will be staged. Commonly matches the AppName. + public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, MCPPlatform platform, string stagingDirRelativePath) + { + OwnerCommand = InOwnerCommand; + AppName = InAppName; + _ManifestFilename = null; + McpConfigKey = InMcpConfigKey; + AppID = InAppID; + BuildVersion = InBuildVersion; + Platform = platform; + string BuildRootPath = GetBuildRootPath(); + StagingDir = CommandUtils.CombinePaths(BuildRootPath, stagingDirRelativePath, BuildVersion, Platform.ToString()); + CloudDirRelativePath = CommandUtils.CombinePaths(stagingDirRelativePath, "CloudDir"); + CloudDir = CommandUtils.CombinePaths(BuildRootPath, CloudDirRelativePath); + Metadata = null; + } + + /// + /// Basic constructor with staging dir suffix override, basically to avoid having platform concatenated + /// + public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, UnrealTargetPlatform InPlatform, string StagingDirRelativePath, string StagingDirSuffix, string InManifestFilename) + : this(InOwnerCommand, InAppName, InMcpConfigKey, InAppID, InBuildVersion, ToMCPPlatform(InPlatform), StagingDirRelativePath, StagingDirSuffix, InManifestFilename) + { + } + + /// + /// Basic constructor with staging dir suffix override, basically to avoid having platform concatenated + /// + /// The automation tool BuildCommand that is currently executing. + /// The name of the app we're working with + /// An identifier for the back-end environment to allow for test deployments, QA, production etc. + /// An identifier for the app. This is deprecated, and can safely be set to zero for all apps. + /// The build version for this build. + /// The platform the build will be deployed to. + /// Relative path from the BuildRootPath where files will be staged. Commonly matches the AppName. + /// By default, we assumed source builds are at build_root/stagingdirrelativepath/buildversion. If they're in a subfolder of this path, specify it here. + /// If specified, will override the value returned by the ManifestFilename property + public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, string InMcpConfigKey, int InAppID, string InBuildVersion, MCPPlatform InPlatform, string StagingDirRelativePath, string StagingDirSuffix, string InManifestFilename) + { + OwnerCommand = InOwnerCommand; + AppName = InAppName; + McpConfigKey = InMcpConfigKey; + AppID = InAppID; + BuildVersion = InBuildVersion; + Platform = InPlatform; + _ManifestFilename = InManifestFilename; + + string BuildRootPath = GetBuildRootPath(); + StagingDir = CommandUtils.CombinePaths(BuildRootPath, StagingDirRelativePath, BuildVersion, StagingDirSuffix); + CloudDirRelativePath = CommandUtils.CombinePaths(StagingDirRelativePath, "CloudDir"); + CloudDir = CommandUtils.CombinePaths(BuildRootPath, CloudDirRelativePath); + Metadata = null; + } + + /// + /// Constructor which supports being able to just simply call BuildPatchToolBase.Get().Execute + /// + public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, int InAppID, string InBuildVersion, MCPPlatform InPlatform, string InCloudDir) + { + OwnerCommand = InOwnerCommand; + AppName = InAppName; + AppID = InAppID; + BuildVersion = InBuildVersion; + Platform = InPlatform; + CloudDir = InCloudDir; + Metadata = null; + } + + /// + /// Constructor which sets all values directly, without assuming any default paths. + /// + public BuildPatchToolStagingInfo(BuildCommand InOwnerCommand, string InAppName, int InAppID, string InBuildVersion, MCPPlatform InPlatform, DirectoryReference InStagingDir, DirectoryReference InCloudDir, string InManifestFilename = null) + { + OwnerCommand = InOwnerCommand; + AppName = InAppName; + AppID = InAppID; + BuildVersion = InBuildVersion; + Platform = InPlatform; + Metadata = null; + if (InStagingDir != null) + { + StagingDir = InStagingDir.FullName; + } + if (InCloudDir != null) + { + DirectoryReference BuildRootDir = new DirectoryReference(GetBuildRootPath()); + if(!InCloudDir.IsUnderDirectory(BuildRootDir)) + { + throw new AutomationException("Cloud directory must be under build root path ({0})", BuildRootDir.FullName); + } + CloudDir = InCloudDir.FullName; + CloudDirRelativePath = InCloudDir.MakeRelativeTo(BuildRootDir).Replace('\\', '/'); + } + if (!string.IsNullOrEmpty(InManifestFilename)) + { + _ManifestFilename = InManifestFilename; + } + } + + /// + /// Associates a piece of metadata (in the form of a key value pair) with the build info. + /// + /// The key to store the metadata against. + /// The value of the metadata to associate. + /// Optional, specifies whether to overwrite the existing key if it exists, default true. + /// The BuildPatchToolStagingInfo to facilitate fluent syntax. + public BuildPatchToolStagingInfo WithMetadata(string Key, string Value, bool bClobber = true) + { + BuildMetadataBase NewMeta = BuildInfoPublisherBase.Get().CreateBuildMetadata(string.Format(@"{{""{0}"":""{1}""}}", Key, Value)); + return WithMetadata(NewMeta, bClobber); + } + + /// + /// Associates new metadata with the build info. + /// + /// The metadata object to merge in. + /// Optional, specifies whether to overwrite the existing key if it exists, default true. + /// The BuildPatchToolStagingInfo to facilitate fluent syntax. + public BuildPatchToolStagingInfo WithMetadata(BuildMetadataBase NewMetadata, bool bClobber = true) + { + if (Metadata != null) + { + Metadata.MergeWith(NewMetadata, bClobber); + } + else + { + Metadata = NewMetadata; + } + return this; + } + + /// + /// Returns the manifest filename, querying build info for it if necessary. + /// If ManifestFilename has already set (either during construction or by a previous call to this method) the cached value will be returned unless bForce is specified. + /// Otherwise, a query will be made to the specified build info service to retrieve the correct manifest filename. + /// + /// Name of which MCP config to check against. + /// If specified, a call will be made to build info even if we have a locally cached version. + /// The manifest filename + public string RetrieveManifestFilename(string McpConfigName, bool bForce = false) + { + if (bForce || string.IsNullOrEmpty(_ManifestFilename)) + { + BuildInfoPublisherBase BI = BuildInfoPublisherBase.Get(); + string ManifestUrl = BI.GetBuildManifestUrl(this, McpConfigName); + if (string.IsNullOrEmpty(ManifestUrl)) + { + throw new AutomationException("Could not determine manifest Url for {0} version {1} from {2} environment.", this.AppName, this.BuildVersion, McpConfigName); + } + _ManifestFilename = ManifestUrl.Split(new char[] { '/', '\\' }).Last(); + } + + return _ManifestFilename; + } + + /// + /// Adds and returns the metadata for this build, querying build info for any additional fields. + /// + /// Optional, name of which MCP config to check against, default null which will use the one stored in this BuildPatchToolStagingInfo. + /// Optional, specifies whether to overwrite existing metadata keys with those received, default false. + /// The Metadata dictionary + public BuildMetadataBase RetrieveBuildMetadata(string McpConfigName = null, bool bClobber = false) + { + BuildInfoPublisherBase BI = BuildInfoPublisherBase.Get(); + return BI.GetBuildMetaData(this, McpConfigName ?? McpConfigKey, bClobber); + } + } + + /// + /// Class that provides programmatic access to the BuildPatchTool + /// + public abstract class BuildPatchToolBase + { + /// + /// Controls which version of BPT to use when executing. + /// + public enum ToolVersion + { + /// + /// The current live, tested build. + /// + Live, + /// + /// The latest published build, may be untested. + /// + Next, + /// + /// An experimental build, for use when one project needs early access or unique changes. + /// + Experimental, + /// + /// Use local build from source of BuildPatchTool. + /// + Source + } + + /// + /// An interface which provides the required Perforce access implementations + /// + public interface IPerforce + { + /// + /// Property to say whether Perforce access is enabled in the environment. + /// + bool bIsEnabled { get; } + + /// + /// Check a file exists in Perforce. + /// + /// Filename to check. + /// True if the file exists in P4. + bool FileExists(string Filename); + + /// + /// Gets the contents of a particular file in the depot and writes it to a local file without syncing it. + /// + /// Depot path to the file (with revision/range if necessary). + /// Output file to write to. + /// True if successful. + bool PrintToFile(string DepotPath, string Filename); + + /// + /// Retrieve the latest CL number for the given file. + /// + /// The filename for the file to check. + /// Receives the CL number. + /// True if the file exists and ChangeList was set. + bool GetLatestChange(string Filename, out int ChangeList); + } + + public class PatchGenerationOptions + { + /// + /// By default, we will only consider data referenced from manifests modified within five days to be reusable. + /// + private const int DEFAULT_DATA_AGE_THRESHOLD = 5; + + public PatchGenerationOptions() + { + DataAgeThreshold = DEFAULT_DATA_AGE_THRESHOLD; + ChunkWindowSize = 1048576; + } + + /// + /// A unique integer for this product. + /// Deprecated. Can be safely left as 0. + /// + public int AppId; + /// + /// The app name for this build, which will be embedded inside the generated manifest to identify the application. + /// + public string AppName; + /// + /// The build version being generated. + /// + public string BuildVersion; + /// + /// Used as part of the build version string. + /// + public MCPPlatform Platform; + /// + /// The directory containing the build image to be read. + /// + public string BuildRoot; + /// + /// The directory which will receive the generated manifest and chunks. + /// + public string CloudDir; + /// + /// The name of the manifest file that will be produced. + /// + public string ManifestFilename; + /// + /// A path to a text file containing BuildRoot relative files to be included in the build. + /// + public string FileInputList; + /// + /// A path to a text file containing BuildRoot relative files to be excluded from the build. + /// + public string FileIgnoreList; + /// + /// A path to a text file containing quoted BuildRoot relative files followed by optional attributes such as readonly compressed executable tag:mytag, separated by \r\n line endings. + /// These attribute will be applied when build is installed client side. + /// + public string FileAttributeList; + /// + /// The path to the app executable, must be relative to, and inside of BuildRoot. + /// + public string AppLaunchCmd; + /// + /// The commandline to send to the app on launch. + /// + public string AppLaunchCmdArgs; + /// + /// The list of prerequisite Ids that this prerequisite installer satisfies. + /// + public List PrereqIds; + /// + /// The prerequisites installer to launch on successful product install, must be relative to, and inside of BuildRoot. + /// + public string PrereqPath; + /// + /// The commandline to send to prerequisites installer on launch. + /// + public string PrereqArgs; + /// + /// When identifying existing patch data to reuse in this build, only + /// files referenced from a manifest file modified within this number of days will be considered for reuse. + /// IMPORTANT: This should always be smaller than the minimum age at which manifest files can be deleted by any cleanup process, to ensure + /// that we do not reuse any files which could be deleted by a concurrently running compactify. It is recommended that this number be at least + /// two days less than the cleanup data age threshold. + /// + public int DataAgeThreshold; + /// + /// Specifies in bytes, the data window size that should be used when saving new chunks. Default is 1048576 (1MiB). + /// + public int ChunkWindowSize; + /// + /// Specifies the desired output FeatureLevel of BuildPatchTool, if this is not provided BPT will warn and default to LatestJson so that project scripts can be updated. + /// + public string FeatureLevel; + /// + /// Contains a list of custom string arguments to be embedded in the generated manifest file. + /// + public List> CustomStringArgs; + /// + /// Contains a list of custom integer arguments to be embedded in the generated manifest file. + /// + public List> CustomIntArgs; + /// + /// Contains a list of custom float arguments to be embedded in the generated manifest file. + /// + public List> CustomFloatArgs; + } + + /// + /// Represents the options passed to the compactify process + /// + public class CompactifyOptions + { + private const int DEFAULT_DATA_AGE_THRESHOLD = 2; + + public CompactifyOptions() + { + DataAgeThreshold = DEFAULT_DATA_AGE_THRESHOLD; + } + + /// + /// BuildPatchTool will run a compactify on this directory. + /// + public string CompactifyDirectory; + /// + /// Corresponds to the -preview parameter + /// + public bool bPreviewCompactify; + /// + /// Patch data files modified within this number of days will *not* be deleted, to ensure that any patch files being written out by a. + /// patch generation process are not deleted before their corresponding manifest file(s) can be written out. + /// NOTE: this should be set to a value larger than the expected maximum time that a build could take. + /// + public int DataAgeThreshold; + } + + public class DataEnumerationOptions + { + /// + /// The file path to the manifest to enumerate from. + /// + public string ManifestFile; + /// + /// The file path to where the list will be saved out, containing \r\n separated cloud relative file paths. + /// + public string OutputFile; + /// + /// When true, the output will include the size of each file on each line, separated by \t + /// + public bool bIncludeSize; + } + + public class ManifestMergeOptions + { + /// + /// The file path to the base manifest. + /// + public string ManifestA; + /// + /// The file path to the update manifest. + /// + public string ManifestB; + /// + /// The file path to the output manifest. + /// + public string ManifestC; + /// + /// The new version string for the build being produced. + /// + public string BuildVersion; + /// + /// Optional. The set of files that should be kept from ManifestA. + /// + public HashSet FilesToKeepFromA; + /// + /// Optional. The set of files that should be kept from ManifestB. + /// + public HashSet FilesToKeepFromB; + } + + public class ManifestDiffOptions + { + /// + /// The file path to the base manifest. + /// + public string ManifestA; + /// + /// The install tags to use for ManifestA. + /// InstallTagsA; + /// + /// The file path to the update manifest. + /// + public string ManifestB; + /// + /// The install tags to use for ManifestB. + /// InstallTagsB; + } + + public class ManifestDiffOutput + { + public class ManifestSummary + { + /// + /// The AppName field from the manifest file. + /// + public string AppName; + /// + /// The AppId field from the manifest file. + /// + public uint AppId; + /// + /// The VersionString field from the manifest file. + /// + public string VersionString; + /// + /// The total size of chunks in the build. + /// + public ulong DownloadSize; + /// + /// The total size of disk space required for the build. + /// + public ulong BuildSize; + /// + /// The list of download sizes for each individual install tag that was used. + /// Note that the sum of these can be higher than the actual total due to possibility of shares files. + /// + public Dictionary IndividualTagDownloadSizes; + /// + /// The list of build sizes for each individual install tag that was used. + /// Note that the sum of these can be higher than the actual total due to possibility of shares files. + /// + public Dictionary IndividualTagBuildSizes; + } + public class ManifestDiff + { + /// + /// The list of build relative paths for files which were added by the patch from ManifestA to ManifestB, subject to using the tags that were provided. + /// + public List NewFilePaths; + /// + /// The list of build relative paths for files which were removed by the patch from ManifestA to ManifestB, subject to using the tags that were provided. + /// + public List RemovedFilePaths; + /// + /// The list of build relative paths for files which were changed between ManifestA and ManifestB, subject to using the tags that were provided. + /// + public List ChangedFilePaths; + /// + /// The list of build relative paths for files which were unchanged between ManifestA and ManifestB, subject to using the tags that were provided. + /// + public List UnchangedFilePaths; + /// + /// The list of cloud directory relative paths for all new chunks required by the patch from ManifestA to ManifestB, subject to using the tags that were provided. + /// + public List NewChunkPaths; + /// + /// The required download size for the patch from ManifestA to ManifestB, subject to using the tags that were provided. + /// + public ulong DeltaDownloadSize; + /// + /// The list of delta sizes for each individual install tag that was used. + /// Note that the sum of these can be higher than the actual total due to possibility of shares files. + /// + public Dictionary IndividualTagDeltaSizes; + } + /// + /// The manifest detail for the source build of the differential. + /// + public ManifestSummary ManifestA; + /// + /// The manifest detail for the target build of the differential. + /// + public ManifestSummary ManifestB; + /// + /// The differential details for the patch from ManifestA's build to ManifestB's build. + /// + public ManifestDiff Differential; + } + + public class AutomationTestsOptions + { + /// + /// Optionally specify the tests to run. + /// + public string TestList; + } + + public class PackageChunksOptions + { + /// + /// Specifies the file path to the manifest to enumerate chunks from. + /// + public string ManifestFile; + /// + /// Specifies the file path to a manifest for a previous build, this will be used to filter out chunks. + /// + public string PrevManifestFile; + /// + /// Specifies the file path to the output package. An extension of .chunkdb will be added if not present. + /// + public string OutputFile; + /// + /// An optional parameter which, if present, specifies the directory where chunks to be packaged can be found. + /// If not specified, the manifest file's location will be used as the cloud directory. + /// + public string CloudDir; + /// + /// Optional value, to restrict the maximum size of each output file (in bytes). + /// If not specified, then only one output file will be produced, containing all the data. + /// If specified, then the output files will be generated as Name.part01.chunkdb, Name.part02.chunkdb etc. The part number will have the number of digits + /// required for highest numbered part. + /// + public ulong? MaxOutputFileSize; + /// + /// Optionally provide a list of tagsets to split chunkdb files on. First all data from the tagset at index 0 will be saved, then any extra data needed + /// for tagset at index 1, and so on. Note that this means the chunkdb files produced for tagset at index 1 will not contain some required data for that tagset if + /// the data already got saved out as part of tagset at index 0, and thus the chunkdb files are additive with no dupes. + /// If it is desired that each tagset's chunkdb files contain the duplicate data, then PackageChunks should be executed once per tagset rather than once will all tagsets. + /// An empty string must be included in one of the tagsets to include untagged file data in that tagset. + /// Leaving this variable null will include data for all files. + /// + public List> TagSetSplit; + /// + /// Specifies the desired output FeatureLevel of BuildPatchTool, if this is not provided BPT will default to before optimised deltas. + /// + public string FeatureLevel; + } + + public class PackageChunksOutput + { + /// + /// The list of full filepaths of all created chunkdb files. + /// + public List ChunkDbFilePaths; + /// + /// If PackageChunksOptions.TagSetSplit was provided, then this variable will contain a lookup table of TagSetSplit index to List of ChunkDbFilePaths indices. + /// e.g. + /// TagSetLookupTable[0] = [ 0, 1, 2, 3, ..., n ] + /// TagSetLookupTable[1] = [] an empty List would mean that all data for this tagset was already included in the chunkdb(s) for previous tagset(s). + /// TagSetLookupTable[2] = [ n+1, n+2, ..., n+m ] + /// + public List> TagSetLookupTable; + } + + public class ChunkDeltaOptimiseOptions + { + /// + /// The file path to the base manifest. + /// + public string SourceManifest; + /// + /// The file path to the update manifest. New data will be added to the cloud directory that this manifest is in. + /// + public string DestinationManifest; + } + + static BuildPatchToolBase Handler = null; + + public static BuildPatchToolBase Get() + { + if (Handler == null) + { + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.SafeGetLoadedTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(BuildPatchToolBase) && typeof(BuildPatchToolBase).IsAssignableFrom(PotentialConfigType)) + { + Handler = Activator.CreateInstance(PotentialConfigType) as BuildPatchToolBase; + break; + } + } + } + if (Handler == null) + { + throw new AutomationException("Attempt to use BuildPatchToolBase.Get() and it doesn't appear that there are any modules that implement this class."); + } + } + return Handler; + } + + /// + /// Runs the Build Patch Tool executable to generate patch data using the supplied parameters. + /// + /// Parameters which will be passed to the Build Patch Tool generation process. + /// Which version of BuildPatchTool is desired. + /// If set to true, will allow an existing manifest file to be overwritten with this execution. Default is false. + public abstract void Execute(PatchGenerationOptions Opts, ToolVersion Version = ToolVersion.Live, bool bAllowManifestClobbering = false); + + /// + /// Runs the Build Patch Tool executable to compactify a cloud directory using the supplied parameters. + /// + /// Parameters which will be passed to the Build Patch Tool compactify process. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(CompactifyOptions Opts, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to enumerate patch data files referenced by a manifest using the supplied parameters. + /// + /// Parameters which will be passed to the Build Patch Tool enumeration process. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(DataEnumerationOptions Opts, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to merge two manifest files producing a hotfix manifest. + /// + /// Parameters which will be passed to the Build Patch Tool manifest merge process. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(ManifestMergeOptions Opts, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to diff two manifest files logging out details. + /// + /// Parameters which will be passed to the Build Patch Tool manifest diff process. + /// Will receive the data back for the diff. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(ManifestDiffOptions Opts, out ManifestDiffOutput Output, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to evaluate built in automation testing. + /// + /// Parameters which will be passed to the Build Patch Tool automation tests process. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(AutomationTestsOptions Opts, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to create ChunkDB file(s) consisting of multiple chunks to allow installing / patching to a specific build. + /// + /// Parameters which will be passed to the Build Patch Tool package chunks process. + /// Will receive the data back for the packaging. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(PackageChunksOptions Opts, out PackageChunksOutput Output, ToolVersion Version = ToolVersion.Live); + + /// + /// Runs the Build Patch Tool executable to create optimised delta files which reduce download size for patching between two specific builds. + /// + /// Parameters which will be passed to the Build Patch Tool chunk delta optimise process. + /// Which version of BuildPatchTool is desired. + public abstract void Execute(ChunkDeltaOptimiseOptions Opts, ToolVersion Version = ToolVersion.Live); + } + + + /// + /// Class that provides programmatic access to the metadata field from build info. + /// + public abstract class BuildMetadataBase + { + /// + /// Merges provided metadata object into this one. + /// + /// The other metadata to merge in. + /// Optional, specifies whether to overwrite existing metadata keys with those in Other, default true. + /// this object to facilitate fluent syntax. + abstract public BuildMetadataBase MergeWith(BuildMetadataBase Other, bool bClobber = true); + } + + /// + /// Helper class + /// + public abstract class BuildInfoPublisherBase + { + static BuildInfoPublisherBase Handler = null; + + public static BuildInfoPublisherBase Get() + { + if (Handler == null) + { + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.SafeGetLoadedTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(BuildInfoPublisherBase) && typeof(BuildInfoPublisherBase).IsAssignableFrom(PotentialConfigType)) + { + Handler = Activator.CreateInstance(PotentialConfigType) as BuildInfoPublisherBase; + break; + } + } + } + if (Handler == null) + { + throw new AutomationException("Attempt to use BuildInfoPublisherBase.Get() and it doesn't appear that there are any modules that implement this class."); + } + } + return Handler; + } + + /// + /// Creates a metadata object implementation, initialized with the provided JSON object. + /// + /// JSON object representation to initialize with. + /// new instance of metadata implementation. + abstract public BuildMetadataBase CreateBuildMetadata(string JsonRepresentation); + + /// + /// Determines whether a given build is registered in build info + /// + /// The staging info representing the build to check. + /// Name of which MCP config to check against. + /// true if the build is registered, false otherwise + abstract public bool BuildExists(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); + + /// + /// Given a MCPStagingInfo defining our build info, posts the build to the MCP BuildInfo Service. + /// + /// Staging Info describing the BuildInfo to post. + abstract public void PostBuildInfo(BuildPatchToolStagingInfo stagingInfo); + + /// + /// Given a MCPStagingInfo defining our build info and a MCP config name, posts the build to the requested MCP BuildInfo Service. + /// + /// Staging Info describing the BuildInfo to post. + /// Name of which MCP config to post to. + abstract public void PostBuildInfo(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); + + /// + /// Given a BuildVersion defining our a build, return the labels applied to that build + /// + /// Build version to return labels for. + /// Which BuildInfo backend to get labels from for this promotion attempt. + /// The list of build labels applied. + abstract public List GetBuildLabels(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); + + /// + /// Given a staging info defining our build, return the manifest url for that registered build + /// + /// Staging Info describing the BuildInfo to query. + /// Name of which MCP config to query. + /// The manifest url. + abstract public string GetBuildManifestUrl(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); + + /// + /// Given a staging info defining our build, return the manifest url for that registered build + /// + /// Application name to check the label in + /// Build version to manifest for. + /// Name of which MCP config to query. + /// + abstract public string GetBuildManifestUrl(string AppName, string BuildVersionWithPlatform, string McpConfigName); + + /// + /// Given a staging info defining our build, return the manifest hash for that registered build + /// + /// Staging Info describing the BuildInfo to query. + /// Name of which MCP config to query. + /// Manifest SHA1 hash as a hex string + abstract public string GetBuildManifestHash(BuildPatchToolStagingInfo StagingInfo, string McpConfigName); + + /// + /// Given a staging info defining our build, fetch and apply the metadata for that registered build. + /// + /// Staging Info describing the BuildInfo to query. + /// Name of which MCP config to query. + /// Optional, specifies whether to overwrite existing metadata keys with those received, default false. + /// The metadata object for convenience. + abstract public BuildMetadataBase GetBuildMetaData(BuildPatchToolStagingInfo StagingInfo, string McpConfigName, bool bClobber = false); + + /// + /// Get a label string for the specific Platform requested. + /// + /// Base of label + /// Platform to add to base label. + /// The label string including platform postfix. + abstract public string GetLabelWithPlatform(string DestinationLabel, MCPPlatform Platform); + + /// + /// Get a BuildVersion string with the Platform concatenated on. + /// + /// Base of label + /// Platform to add to base label. + /// The BuildVersion string including platform postfix. + abstract public string GetBuildVersionWithPlatform(BuildPatchToolStagingInfo StagingInfo); + + /// + /// Get the BuildVersion for a build that is labeled under a specific appname. + /// + /// Application name to check the label in + /// Label name to get the build version for + /// Which BuildInfo backend to label the build in. + /// The BuildVersion or null if no build labeled. + abstract public string GetLabeledBuildVersion(string AppName, string LabelName, string McpConfigName); + + /// + /// Apply the requested label to the requested build in the BuildInfo backend for the requested MCP environment + /// + /// Staging info for the build to label. + /// Label, including platform, to apply + /// Which BuildInfo backend to label the build in. + abstract public void LabelBuild(BuildPatchToolStagingInfo StagingInfo, string DestinationLabelWithPlatform, string McpConfigName); + + /// + /// Informs Patcher Service of a new build availability after async labeling is complete + /// (this usually means the build was copied to a public file server before the label could be applied). + /// + /// Parent command + /// Application name that the patcher service will use. + /// BuildVersion string that the patcher service will use. + /// Relative path to the Manifest file relative to the global build root (which is like P:\Builds) + /// Name of the label that we will be setting. + abstract public void BuildPromotionCompleted(BuildPatchToolStagingInfo stagingInfo, string AppName, string BuildVersion, string ManifestRelativePath, string PlatformName, string LabelName); + } + + /// + /// Helpers for using the MCP account service + /// + public abstract class McpAccountServiceBase + { + static McpAccountServiceBase Handler = null; + + public static McpAccountServiceBase Get() + { + if (Handler == null) + { + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.SafeGetLoadedTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(McpAccountServiceBase) && typeof(McpAccountServiceBase).IsAssignableFrom(PotentialConfigType)) + { + Handler = Activator.CreateInstance(PotentialConfigType) as McpAccountServiceBase; + break; + } + } + } + if (Handler == null) + { + throw new AutomationException("Attempt to use McpAccountServiceBase.Get() and it doesn't appear that there are any modules that implement this class."); + } + } + return Handler; + } + + /// + /// Gets an OAuth client token for an environment using the default client id and client secret + /// + /// A descriptor for the environment we want a token for + /// An OAuth client token for the specified environment. + public string GetClientToken(McpConfigData McpConfig) + { + return GetClientToken(McpConfig, McpConfig.ClientId, McpConfig.ClientSecret); + } + + /// + /// Gets an OAuth client token using the default client id and client secret and the environment for the specified staging info + /// + /// The staging info for the build we're working with. This will be used to determine the correct back-end service. + /// + public string GetClientToken(BuildPatchToolStagingInfo StagingInfo) + { + McpConfigData McpConfig = McpConfigMapper.FromStagingInfo(StagingInfo); + return GetClientToken(McpConfig); + } + + /// + /// Gets an OAuth client token for an environment using the specified client id and client secret + /// + /// A descriptor for the environment we want a token for + /// The client id used to obtain the token + /// The client secret used to obtain the token + /// An OAuth client token for the specified environment. + public abstract string GetClientToken(McpConfigData McpConfig, string ClientId, string ClientSecret); + + public abstract string SendWebRequest(WebRequest Upload, string Method, string ContentType, byte[] Data); + } + + /// + /// Helper class to manage files stored in some arbitrary cloud storage system + /// + public abstract class CloudStorageBase + { + private static readonly object LockObj = new object(); + private static Dictionary Handlers = new Dictionary(); + private const string DEFAULT_INSTANCE_NAME = "DefaultInstance"; + + /// + /// Gets the default instance of CloudStorageBase + /// + /// A default instance of CloudStorageBase. The first time each instance is returned, it will require initialization with its Init() method. + public static CloudStorageBase Get() + { + return GetByNameImpl(DEFAULT_INSTANCE_NAME); // Identifier for the default cloud storage + } + + /// + /// Gets an instance of CloudStorageBase. + /// Multiple calls with the same instance name will return the same object. + /// + /// The name of the object to return + /// An instance of CloudStorageBase. The first time each instance is returned, it will require initialization with its Init() method. + public static CloudStorageBase GetByName(string InstanceName) + { + if (InstanceName == DEFAULT_INSTANCE_NAME) + { + CommandUtils.LogWarning("CloudStorageBase.GetByName called with {0}. This will return the same instance as Get().", DEFAULT_INSTANCE_NAME); + } + return GetByNameImpl(InstanceName); + } + + private static CloudStorageBase GetByNameImpl(string InstanceName) + { + CloudStorageBase Result = null; + if (!Handlers.TryGetValue(InstanceName, out Result)) + { + lock (LockObj) + { + if (Handlers.ContainsKey(InstanceName)) + { + Result = Handlers[InstanceName]; + } + else + { + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.SafeGetLoadedTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(CloudStorageBase) && typeof(CloudStorageBase).IsAssignableFrom(PotentialConfigType)) + { + Result = Activator.CreateInstance(PotentialConfigType) as CloudStorageBase; + Handlers.Add(InstanceName, Result); + break; + } + } + } + } + } + if (Result == null) + { + throw new AutomationException("Could not find any modules which provide an implementation of CloudStorageBase."); + } + } + return Result; + } + + /// + /// Initializes the provider. + /// Configuration data to initialize the provider. The exact format of the data is provider specific. It might, for example, contain an API key. + /// + abstract public void Init(Dictionary Config, bool bForce = false); + + /// + /// Retrieves a file from the cloud storage provider + /// + /// The name of the folder or container from which contains the file being checked. + /// The identifier or filename of the file to check. + /// If set to true, all log output for the operation is suppressed. + /// True if the file exists in cloud storage, false otherwise. + abstract public bool FileExists(string Container, string Identifier, bool bQuiet = false); + + /// + /// Retrieves a file from the cloud storage provider and saves it to disk. + /// + /// The name of the folder or container from which to retrieve the file. + /// The identifier or filename of the file to retrieve. + /// The full path to the name of the file to save + /// An OUTPUT parameter containing the content's type (null if the cloud provider does not provide this information) + /// If false, and the OutputFile already exists, an error will be thrown. + /// The number of bytes downloaded. + abstract public long DownloadFile(string Container, string Identifier, string OutputFile, out string ContentType, bool bOverwrite = false); + + /// + /// Retrieves a file from the cloud storage provider. + /// + /// The name of the folder or container from which to retrieve the file. + /// The identifier or filename of the file to retrieve. + /// An OUTPUT parameter containing the content's type (null if the cloud provider does not provide this information) + /// A byte array containing the file's contents. + abstract public byte[] GetFile(string Container, string Identifier, out string ContentType); + + /// + /// Posts a file to the cloud storage provider. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// A byte array containing the data to write. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + public PostFileResult PostFile(string Container, string Identifier, byte[] Contents, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null) + { + return PostFileAsync(Container, Identifier, Contents, ContentType, bOverwrite, bMakePublic, bQuiet, Metadata).Result; + } + + /// + /// Posts a file to the cloud storage provider asynchronously. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// A byte array containing the data to write. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + abstract public Task PostFileAsync(string Container, string Identifier, byte[] Contents, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null); + + /// + /// Posts a file to the cloud storage provider. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// The full path of the file to upload. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + public PostFileResult PostFile(string Container, string Identifier, string SourceFilePath, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null) + { + return PostFileAsync(Container, Identifier, SourceFilePath, ContentType, bOverwrite, bMakePublic, bQuiet, Metadata).Result; + } + + /// + /// Posts a file to the cloud storage provider asynchronously. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// The full path of the file to upload. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + abstract public Task PostFileAsync(string Container, string Identifier, string SourceFilePath, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null); + + /// + /// Posts a file to the cloud storage provider using multiple connections. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// The full path of the file to upload. + /// The number of concurrent connections to use during uploading. + /// The size of each part that is uploaded. Minimum (and default) is 5 MB. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + public PostFileResult PostMultipartFile(string Container, string Identifier, string SourceFilePath, int NumConcurrentConnections, decimal PartSizeMegabytes = 5.0m, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null) + { + return PostMultipartFileAsync(Container, Identifier, SourceFilePath, NumConcurrentConnections, PartSizeMegabytes, ContentType, bOverwrite, bMakePublic, bQuiet, Metadata).Result; + } + + /// + /// Posts a file to the cloud storage provider using multiple connections asynchronously. + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + /// The full path of the file to upload. + /// The number of concurrent connections to use during uploading. + /// The size of each part that is uploaded. Minimum (and default) is 5 MB. + /// The MIME type of the file being uploaded. If left NULL, will be determined server-side by cloud provider. + /// If true, will overwrite an existing file. If false, will throw an exception if the file exists. + /// Specifies whether the file should be made publicly readable. + /// If set to true, all log output for the operation is supressed. + /// If not null, key-value pairs of metadata to be applied to the object. + /// A PostFileResult indicating whether the call was successful, and the URL to the uploaded file. + abstract public Task PostMultipartFileAsync(string Container, string Identifier, string SourceFilePath, int NumConcurrentConnections, decimal PartSizeMegabytes = 5.0m, string ContentType = null, bool bOverwrite = true, bool bMakePublic = false, bool bQuiet = false, IDictionary Metadata = null); + + /// + /// Deletes a file from cloud storage + /// + /// The name of the folder or container in which to store the file. + /// The identifier or filename of the file to write. + abstract public void DeleteFile(string Container, string Identifier); + + /// + /// Deletes a folder from cloud storage + /// + /// The name of the folder or container from which to delete the file. + /// The identifier or name of the folder to delete. + abstract public void DeleteFolder(string Container, string FolderIdentifier); + + /// + /// Retrieves a list of folders from the cloud storage provider + /// + /// The name of the container from which to list folders. + /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. + /// An action which acts upon an options object to configure the operation. See ListOptions for more details. + /// An array of paths to the folders in the specified container and matching the prefix constraint. + public string[] ListFolders(string Container, string Prefix, Action Options) + { + ListOptions Opts = new ListOptions(); + if (Options != null) + { + Options(Opts); + } + return ListFolders(Container, Prefix, Opts); + } + + /// + /// Retrieves a list of folders from the cloud storage provider + /// + /// The name of the container from which to list folders. + /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. + /// An options object to configure the operation. See ListOptions for more details. + /// An array of paths to the folders in the specified container and matching the prefix constraint. + abstract public string[] ListFolders(string Container, string Prefix, ListOptions Options); + + /// + /// DEPRECATED. Retrieves a list of files from the cloud storage provider. See overload with ListOptions for non-deprecated use. + /// + /// The name of the folder or container from which to list files. + /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. + /// Indicates whether the list of files returned should traverse subdirectories + /// If set to true, all log output for the operation is supressed. + /// An array of paths to the files in the specified location and matching the prefix constraint. + public string[] ListFiles(string Container, string Prefix = null, bool bRecursive = true, bool bQuiet = false) + { + return ListFiles(Container, Prefix, opts => + { + opts.bRecursive = bRecursive; + opts.bQuiet = bQuiet; + }); + } + + /// + /// Retrieves a list of files from the cloud storage provider + /// + /// The name of the container from which to list folders. + /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. + /// An action which acts upon an options object to configure the operation. See ListOptions for more details. + /// An array of paths to the folders in the specified container and matching the prefix constraint. + public string[] ListFiles(string Container, string Prefix, Action Options) + { + ListOptions Opts = new ListOptions(); + if (Options != null) + { + Options(Opts); + } + return ListFiles(Container, Prefix, Opts); + } + + /// + /// Retrieves a list of files from the cloud storage provider. + /// + /// The name of the folder or container from which to list files. + /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. + /// An options object to configure the operation. See ListOptions for more details. + /// An array of paths to the files in the specified location and matching the prefix constraint. + abstract public string[] ListFiles(string Container, string Prefix, ListOptions Options); + + /// + /// Retrieves a list of files together with basic metadata from the cloud storage provider + /// + /// The name of the container from which to list folders. + /// A string to specify the identifer that you want to list from. Typically used to specify a relative folder within the container to list all of its folders. Specify null to return folders in the root of the container. + /// An action which acts upon an options object to configure the operation. See ListOptions for more details. + /// An array of metadata objects (including filenames) to the files in the specified location and matching the prefix constraint. + public ObjectMetadata[] ListFilesWithMetadata(string Container, string Prefix, Action Options) + { + ListOptions Opts = new ListOptions(); + if (Options != null) + { + Options(Opts); + } + return ListFilesWithMetadata(Container, Prefix, Opts); + } + + /// + /// Retrieves a list of files together with basic metadata from the cloud storage provider. + /// + /// The name of the folder or container from which to list files. + /// A string with which the identifier or filename should start. Typically used to specify a relative directory within the container to list all of its files recursively. Specify null to return all files. + /// An options object to configure the operation. See ListOptions for more details. + /// An array of metadata objects (including filenames) to the files in the specified location and matching the prefix constraint. + abstract public ObjectMetadata[] ListFilesWithMetadata(string Container, string Prefix, ListOptions Options); + + /// + /// Sets one or more items of metadata on an object in cloud storage + /// + /// The name of the folder or container in which the file is stored. + /// The identifier of filename of the file to set metadata on. + /// A dictionary containing the metadata keys and their values + /// If true, then existing metadata will be replaced (or overwritten if the keys match). If false, no existing metadata is retained. + abstract public void SetMetadata(string Container, string Identifier, IDictionary Metadata, bool bMerge = true); + + /// + /// Gets all items of metadata on an object in cloud storage. Metadata values are all returned as strings. + /// + /// The name of the folder or container in which the file is stored. + /// The identifier of filename of the file to get metadata. + abstract public Dictionary GetMetadata(string Container, string Identifier); + + /// + /// Gets an item of metadata from an object in cloud storage. The object is casted to the specified type. + /// + /// The name of the folder or container in which the file is stored. + /// The identifier of filename of the file to get metadata. + /// The key of the item of metadata to retrieve. + abstract public T GetMetadata(string Container, string Identifier, string MetadataKey); + + /// + /// Updates the timestamp on a particular file in cloud storage to the current time. + /// + /// The name of the container in which the file is stored. + /// The identifier of filename of the file to touch. + abstract public void TouchFile(string Container, string Identifier); + + /// + /// Copies manifest and chunks from a staged location to cloud storage. + /// + /// The name of the container in which to store files. + /// Staging info used to determine where the chunks are to copy. + /// If true, will always copy the manifest and chunks to cloud storage. Otherwise, will only copy if the manifest isn't already present on cloud storage. + /// True if the build was copied to cloud storage, false otherwise. + abstract public bool CopyChunksToCloudStorage(string Container, BuildPatchToolStagingInfo StagingInfo, bool bForce = false); + + /// + /// Copies manifest and its chunks from a specific path to a given target folder in the cloud. + /// + /// The name of the container in which to store files. + /// The path within the container that the files should be stored in. + /// The full path of the manifest file to copy. + /// If true, will always copy the manifest and chunks to cloud storage. Otherwise, will only copy if the manifest isn't already present on cloud storage. + /// True if the build was copied to cloud storage, false otherwise. + abstract public bool CopyChunksToCloudStorage(string Container, string RemoteCloudDir, string ManifestFilePath, bool bForce = false); + + /// + /// Verifies whether a manifest for a given build is in cloud storage. + /// + /// The name of the folder or container in which to store files. + /// Staging info representing the build to check. + /// True if the manifest exists in cloud storage, false otherwise. + abstract public bool IsManifestOnCloudStorage(string Container, BuildPatchToolStagingInfo StagingInfo); + + public class PostFileResult + { + /// + /// Set to the URL of the uploaded file on success + /// + public string ObjectURL { get; set; } + + /// + /// Set to true if the write succeeds, false otherwise. + /// + public bool bSuccess { get; set; } + } + + /// + /// Encapsulates options used when listing files or folders using ListFiles and ListFolders + /// + public class ListOptions + { + public ListOptions() + { + bQuiet = false; + bRecursive = false; + bReturnURLs = true; + } + + /// + /// If set to true, all log output for the operation is suppressed. Defaults to false. + /// + public bool bQuiet { get; set; } + + /// + /// Indicates whether the list of files returned should traverse subfolders. Defaults to false. + /// + public bool bRecursive { get; set; } + + /// + /// If true, returns the full URL to the listed objects. If false, returns their identifier within the container. Defaults to true. + /// + public bool bReturnURLs { get; set; } + } + + public class ObjectMetadata + { + public string ETag { get; set; } + public string Path { get; set; } + public string Url { get; set; } + public DateTime LastModified { get; set; } + public long Size { get; set; } + } + } + + public abstract class CatalogServiceBase + { + static CatalogServiceBase Handler = null; + + public static CatalogServiceBase Get() + { + if (Handler == null) + { + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.GetTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(CatalogServiceBase) && typeof(CatalogServiceBase).IsAssignableFrom(PotentialConfigType)) + { + Handler = Activator.CreateInstance(PotentialConfigType) as CatalogServiceBase; + break; + } + } + } + if (Handler == null) + { + throw new AutomationException("Attempt to use McpCatalogServiceBase.Get() and it doesn't appear that there are any modules that implement this class."); + } + } + return Handler; + } + + public abstract string GetAppName(string ItemId, string[] EngineVersions, string McpConfigName); + public abstract string[] GetNamespaces(string McpConfigName); + public abstract McpCatalogItem GetItemById(string Namespace, string ItemId, string McpConfigName); + public abstract IEnumerable GetAllItems(string Namespace, string McpConfigName); + + public class McpCatalogItem + { + public string Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string LongDescription { get; set; } + public string TechnicalDetails { get; set; } + public string Namespace { get; set; } + public string Status { get; set; } + public DateTime CreationDate { get; set; } + public DateTime LastModifiedDate { get; set; } + public ReleaseInfo[] ReleaseInfo { get; set; } + } + + public class ReleaseInfo + { + public string AppId { get; set; } + public string[] CompatibleApps { get; set; } + public string[] Platform { get; set; } + public DateTime DateAdded { get; set; } + } + } +} + +namespace EpicGames.MCP.Config +{ + /// + /// Class for retrieving MCP configuration data + /// + public class McpConfigHelper + { + // List of configs is cached off for fetching from multiple times + private static Dictionary Configs; + + public static McpConfigData Find(string ConfigName) + { + if (Configs == null) + { + // Load all secret configs by trying to instantiate all classes derived from McpConfig from all loaded DLLs. + // Note that we're using the default constructor on the secret config types. + Configs = new Dictionary(); + Assembly[] LoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var Dll in LoadedAssemblies) + { + Type[] AllTypes = Dll.SafeGetLoadedTypes(); + foreach (var PotentialConfigType in AllTypes) + { + if (PotentialConfigType != typeof(McpConfigData) && typeof(McpConfigData).IsAssignableFrom(PotentialConfigType)) + { + try + { + McpConfigData Config = Activator.CreateInstance(PotentialConfigType) as McpConfigData; + if (Config != null) + { + Configs.Add(Config.Name, Config); + } + } + catch + { + BuildCommand.LogWarning("Unable to create McpConfig: {0}", PotentialConfigType.Name); + } + } + } + } + } + McpConfigData LoadedConfig; + Configs.TryGetValue(ConfigName, out LoadedConfig); + if (LoadedConfig == null) + { + throw new AutomationException("Unable to find requested McpConfig: {0}", ConfigName); + } + return LoadedConfig; + } + } + + // Class for storing mcp configuration data + public class McpConfigData + { + public McpConfigData(string InName, string InAccountBaseUrl, string InFortniteBaseUrl, string InLauncherBaseUrl, string InBuildInfoV2BaseUrl, string InLauncherV2BaseUrl, string InCatalogBaseUrl, string InClientId, string InClientSecret) + { + Name = InName; + AccountBaseUrl = InAccountBaseUrl; + FortniteBaseUrl = InFortniteBaseUrl; + LauncherBaseUrl = InLauncherBaseUrl; + BuildInfoV2BaseUrl = InBuildInfoV2BaseUrl; + LauncherV2BaseUrl = InLauncherV2BaseUrl; + CatalogBaseUrl = InCatalogBaseUrl; + ClientId = InClientId; + ClientSecret = InClientSecret; + } + + public string Name; + public string AccountBaseUrl; + public string FortniteBaseUrl; + public string LauncherBaseUrl; + public string BuildInfoV2BaseUrl; + public string LauncherV2BaseUrl; + public string CatalogBaseUrl; + public string ClientId; + public string ClientSecret; + + public void SpewValues() + { + CommandUtils.LogVerbose("Name : {0}", Name); + CommandUtils.LogVerbose("AccountBaseUrl : {0}", AccountBaseUrl); + CommandUtils.LogVerbose("FortniteBaseUrl : {0}", FortniteBaseUrl); + CommandUtils.LogVerbose("LauncherBaseUrl : {0}", LauncherBaseUrl); + CommandUtils.LogVerbose("BuildInfoV2BaseUrl : {0}", BuildInfoV2BaseUrl); + CommandUtils.LogVerbose("LauncherV2BaseUrl : {0}", LauncherV2BaseUrl); + CommandUtils.LogVerbose("CatalogBaseUrl : {0}", CatalogBaseUrl); + CommandUtils.LogVerbose("ClientId : {0}", ClientId); + // we don't really want this in logs CommandUtils.Log("ClientSecret : {0}", ClientSecret); + } + } + + public class McpConfigMapper + { + static public McpConfigData FromMcpConfigKey(string McpConfigKey) + { + return McpConfigHelper.Find("MainGameDevNet"); + } + + static public McpConfigData FromStagingInfo(EpicGames.MCP.Automation.BuildPatchToolStagingInfo StagingInfo) + { + string McpConfigNameToLookup = null; + if (StagingInfo.OwnerCommand != null) + { + McpConfigNameToLookup = StagingInfo.OwnerCommand.ParseParamValue("MCPConfig"); + } + if (String.IsNullOrEmpty(McpConfigNameToLookup)) + { + return FromMcpConfigKey(StagingInfo.McpConfigKey); + } + return McpConfigHelper.Find(McpConfigNameToLookup); + } + } + +} diff --git a/Engine/Source/Programs/AutomationTool/AutomationUtils/Platform.cs b/Engine/Source/Programs/AutomationTool/AutomationUtils/Platform.cs index 6f5bf824098e..7fd5434ad42c 100644 --- a/Engine/Source/Programs/AutomationTool/AutomationUtils/Platform.cs +++ b/Engine/Source/Programs/AutomationTool/AutomationUtils/Platform.cs @@ -511,7 +511,7 @@ namespace AutomationTool } } - public virtual bool PublishSymbols(DirectoryReference SymbolStoreDirectory, List Files, string Product) + public virtual bool PublishSymbols(DirectoryReference SymbolStoreDirectory, List Files, string Product, string BuildVersion = null) { CommandUtils.LogWarning("PublishSymbols() has not been implemented for {0}", PlatformType.ToString()); return false; diff --git a/Engine/Source/Programs/AutomationTool/AutomationUtils/ProcessUtils.cs b/Engine/Source/Programs/AutomationTool/AutomationUtils/ProcessUtils.cs index 7fb757a67457..035ca496d85a 100644 --- a/Engine/Source/Programs/AutomationTool/AutomationUtils/ProcessUtils.cs +++ b/Engine/Source/Programs/AutomationTool/AutomationUtils/ProcessUtils.cs @@ -812,8 +812,12 @@ namespace AutomationTool LogWithVerbosity(SpewVerbosity,"Running: " + App + " " + (String.IsNullOrEmpty(CommandLine) ? "" : CommandLine)); } - string PrevIndent = Tools.DotNETCommon.Log.Indent; - Tools.DotNETCommon.Log.Indent += " "; + string PrevIndent = null; + if(Options.HasFlag(ERunOptions.AllowSpew)) + { + PrevIndent = Tools.DotNETCommon.Log.Indent; + Tools.DotNETCommon.Log.Indent += " "; + } IProcessResult Result = ProcessManager.CreateProcess(App, Options.HasFlag(ERunOptions.AllowSpew), !Options.HasFlag(ERunOptions.NoStdOutCapture), Env, SpewVerbosity: SpewVerbosity, SpewFilterCallback: SpewFilterCallback); try @@ -862,7 +866,10 @@ namespace AutomationTool } finally { - Tools.DotNETCommon.Log.Indent = PrevIndent; + if(PrevIndent != null) + { + Tools.DotNETCommon.Log.Indent = PrevIndent; + } } if (!Options.HasFlag(ERunOptions.NoWaitForExit)) diff --git a/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/AgeStoreTask.cs b/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/AgeStoreTask.cs index 94d2cf884d40..a90a5c36687e 100644 --- a/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/AgeStoreTask.cs +++ b/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/AgeStoreTask.cs @@ -34,6 +34,12 @@ namespace Win.Automation [TaskParameter] public int Days; + /// + /// The root of the build dir to check for existing buildversion named directories + /// + [TaskParameter(Optional = true)] + public string BuildDir; + /// /// A substring to match in directory file names before deleting symbols. This allows the "age store" task /// to avoid deleting symbols from other builds in the case where multiple builds share the same symbol server. @@ -90,24 +96,61 @@ namespace Win.Automation } } - private void RecurseDirectory(DateTime ExpireTimeUtc, DirectoryInfo CurrentDirectory, string[] DirectoryStructure, int Level, string Filter, bool bDeleteIndividualFiles) + // Checks if an existing build has a version file, returns false to NOT delete if it exists + private static bool CheckCanDeleteFromVersionFile(HashSet ExistingBuilds, DirectoryInfo Directory, FileInfo IndividualFile = null) + { + // check for any existing version files + foreach (FileInfo BuildVersionFile in Directory.EnumerateFiles("*.version")) + { + // If the buildversion matches one of the directories in build share provided, don't delete no matter the age. + string BuildVersion = Path.GetFileNameWithoutExtension(BuildVersionFile.Name); + if (ExistingBuilds.Contains(BuildVersion)) + { + // if checking for an individual file, see if the filename matches what's in the .version file. + // these file names won't have extensions. + if (IndividualFile != null) + { + string IndividualFilePath = IndividualFile.FullName; + string FilePointerName = File.ReadAllText(BuildVersionFile.FullName).Trim(); + if(FilePointerName == Path.GetFileNameWithoutExtension(IndividualFilePath)) + { + CommandUtils.LogInformation("Found existing build {0} in the BuildDir with matching individual file {1} - skipping.", BuildVersion, IndividualFilePath); + return false; + } + } + // otherwise it's okay to just mark the entire folder for delete + else + { + CommandUtils.LogInformation("Found existing build {0} in the BuildDir - skipping.", BuildVersion); + return false; + } + } + } + return true; + } + + private void RecurseDirectory(DateTime ExpireTimeUtc, DirectoryInfo CurrentDirectory, string[] DirectoryStructure, int Level, string Filter, HashSet ExistingBuilds, bool bDeleteIndividualFiles) { // Do a file search at the last level. if (Level == DirectoryStructure.Length) { - // If all files are out of date, delete the directory... - if (CurrentDirectory.EnumerateFiles().All(x => x.LastWriteTimeUtc < ExpireTimeUtc)) - { - TryDelete(CurrentDirectory); - } - else if (bDeleteIndividualFiles) + if (bDeleteIndividualFiles) { // Delete any file in the directory that is out of date. - foreach (FileInfo OutdatedFile in CurrentDirectory.EnumerateFiles().Where(x => x.LastWriteTimeUtc < ExpireTimeUtc)) + foreach (FileInfo OutdatedFile in CurrentDirectory.EnumerateFiles().Where(x => x.LastWriteTimeUtc < ExpireTimeUtc && x.Extension != ".version")) { - TryDelete(OutdatedFile); + // check to make sure this file is valid to delete + if (CheckCanDeleteFromVersionFile(ExistingBuilds, CurrentDirectory, OutdatedFile)) + { + TryDelete(OutdatedFile); + } } } + // If all files are out of date, delete the directory... + else if (CurrentDirectory.EnumerateFiles().Where(x => x.Extension != ".version").All(x => x.LastWriteTimeUtc < ExpireTimeUtc) && CheckCanDeleteFromVersionFile(ExistingBuilds, CurrentDirectory)) + { + TryDelete(CurrentDirectory); + } } else { @@ -118,7 +161,7 @@ namespace Win.Automation foreach (var ChildDirectory in CurrentDirectory.GetDirectories(ReplacedPattern, SearchOption.TopDirectoryOnly)) { - RecurseDirectory(ExpireTimeUtc, ChildDirectory, DirectoryStructure, Level + 1, Filter, bDeleteIndividualFiles); + RecurseDirectory(ExpireTimeUtc, ChildDirectory, DirectoryStructure, Level + 1, Filter, ExistingBuilds, bDeleteIndividualFiles); } } @@ -148,6 +191,25 @@ namespace Win.Automation ? string.Empty : Parameters.Filter.Trim(); + // Eumerate the root directory of builds for buildversions to check against + // Folder names in the root directory should match the name of the .version files + HashSet ExistingBuilds = new HashSet(StringComparer.OrdinalIgnoreCase); + if(!string.IsNullOrWhiteSpace(Parameters.BuildDir)) + { + DirectoryReference BuildDir = new DirectoryReference(Parameters.BuildDir); + if(DirectoryReference.Exists(BuildDir)) + { + foreach (string BuildName in DirectoryReference.EnumerateDirectories(BuildDir).Select(Build => Build.GetDirectoryName())) + { + ExistingBuilds.Add(BuildName); + } + } + else + { + CommandUtils.LogWarning("BuildDir of {0} was provided but it doesn't exist! Will not check buildversions against it.", Parameters.BuildDir); + } + } + // Get the time at which to expire files DateTime ExpireTimeUtc = DateTime.UtcNow - TimeSpan.FromDays(Parameters.Days); CommandUtils.LogInformation("Expiring all files before {0}...", ExpireTimeUtc); @@ -156,7 +218,7 @@ namespace Win.Automation DirectoryReference SymbolServerDirectory = ResolveDirectory(Parameters.StoreDir); CommandUtils.OptionallyTakeLock(TargetPlatform.SymbolServerRequiresLock, SymbolServerDirectory, TimeSpan.FromMinutes(15), () => { - RecurseDirectory(ExpireTimeUtc, new DirectoryInfo(SymbolServerDirectory.FullName), DirectoryStructure, 0, Filter, TargetPlatform.SymbolServerDeleteIndividualFiles); + RecurseDirectory(ExpireTimeUtc, new DirectoryInfo(SymbolServerDirectory.FullName), DirectoryStructure, 0, Filter, ExistingBuilds, TargetPlatform.SymbolServerDeleteIndividualFiles); }); } diff --git a/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/SymStoreTask.cs b/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/SymStoreTask.cs index da483b69e5d2..1e382de9f179 100644 --- a/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/SymStoreTask.cs +++ b/Engine/Source/Programs/AutomationTool/BuildGraph/Tasks/SymStoreTask.cs @@ -40,7 +40,13 @@ namespace AutomationTool /// [TaskParameter] public string Product; - } + + /// + /// BuildVersion associated with these symbols. Used for cleanup in AgeStore by matching this version against a directory name in a build share + /// + [TaskParameter(Optional = true)] + public string BuildVersion; + } /// /// Task which strips symbols from a set of files @@ -80,7 +86,7 @@ namespace AutomationTool Platform TargetPlatform = Platform.GetPlatform(Parameters.Platform); CommandUtils.OptionallyTakeLock(TargetPlatform.SymbolServerRequiresLock, StoreDir, TimeSpan.FromMinutes(60), () => { - if (!TargetPlatform.PublishSymbols(StoreDir, Files, Parameters.Product)) + if (!TargetPlatform.PublishSymbols(StoreDir, Files, Parameters.Product, Parameters.BuildVersion)) { throw new AutomationException("Failure publishing symbol files."); } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Devices/Gauntlet.DevicePool.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Devices/Gauntlet.DevicePool.cs index fabf126216c1..b5fcdf0d06a9 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Devices/Gauntlet.DevicePool.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Devices/Gauntlet.DevicePool.cs @@ -226,6 +226,12 @@ namespace Gauntlet /// private static DevicePool _Instance; + /// + /// The maximum number of problem devices to report to device backend + /// This mitigates issues on builders, such as hung local processes, incorrectly reporting problem devices + /// + private int MaxDeviceErrorReports = 3; + /// /// Protected constructor - code should use DevicePool.Instance /// @@ -430,6 +436,14 @@ namespace Gauntlet /// private void ReportDeviceError(string ServiceDeviceName, string ErrorMessage) { + if (MaxDeviceErrorReports == 0) + { + Log.Warning("Maximum device errors reported to backend, {0} : {1} ignored", ServiceDeviceName, ErrorMessage); + return; + } + + MaxDeviceErrorReports--; + Reservation.ReportDeviceError(DeviceURL, ServiceDeviceName, ErrorMessage); } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Gauntlet.Utils.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Gauntlet.Utils.cs index 11a648905e67..8abcdd94d172 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Gauntlet.Utils.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Gauntlet.Utils.cs @@ -12,6 +12,8 @@ using System.Reflection; using ImageMagick; using UnrealBuildTool; using Tools.DotNETCommon; +using System.Security.Cryptography; +using System.Text; namespace Gauntlet { @@ -373,6 +375,30 @@ namespace Gauntlet } } + public class Hasher + { + public static string ComputeHash(string Input) + { + if (string.IsNullOrEmpty(Input)) + { + return "0"; + } + + HashAlgorithm Hasher = MD5.Create(); //or use SHA256.Create(); + + byte[] Hash = Hasher.ComputeHash(Encoding.UTF8.GetBytes(Input)); + + string HashString = ""; + + foreach (byte b in Hash) + { + HashString += (b.ToString("X2")); + } + + return HashString; + } + } + /* * Helper class that can be used with a using() statement to emit log entries that prevent EC triggering on Error/Warning statements */ @@ -792,11 +818,13 @@ namespace Gauntlet foreach (string RelativePath in DeletionList) { FileInfo DestInfo = new FileInfo(Path.Combine(DestDir.FullName, RelativePath)); - + Log.Verbose("Deleting extra file {0}", DestInfo.FullName); try { + // avoid an UnauthorizedAccessException by making sure file isn't read only + DestInfo.IsReadOnly = false; DestInfo.Delete(); } catch (Exception Ex) @@ -953,6 +981,74 @@ namespace Gauntlet return false; } + + /// + /// Marks a directory for future cleanup + /// + /// + public static void MarkDirectoryForCleanup(string InPath) + { + if (Directory.Exists(InPath) == false) + { + Directory.CreateDirectory(InPath); + } + + // write a token, used to detect and old gauntlet-installed builds periodically + string TokenPath = Path.Combine(InPath, "gauntlet.tempdir"); + File.WriteAllText(TokenPath, "Created by Gauntlet"); + } + + /// + /// Removes any directory at the specified path which has a file matching the provided name + /// older than the specified number of days. Used by code that writes a .token file to temp + /// folders + /// + /// + /// + /// + public static void CleanupMarkedDirectories(string InPath, int Days) + { + DirectoryInfo Di = new DirectoryInfo(InPath); + + if (Di.Exists == false) + { + return; + } + + foreach (DirectoryInfo SubDir in Di.GetDirectories()) + { + bool HasFile = + SubDir.GetFiles().Where(F => { + int DaysOld = (DateTime.Now - F.LastWriteTime).Days; + + if (DaysOld >= Days) + { + // use the old and new tokennames + return string.Equals(F.Name, "gauntlet.tempdir", StringComparison.OrdinalIgnoreCase) || + string.Equals(F.Name, "gauntlet.token", StringComparison.OrdinalIgnoreCase); + } + + return false; + }).Count() > 0; + + if (HasFile) + { + Log.Info("Removing old directory {0}", SubDir.Name); + try + { + SubDir.Delete(true); + } + catch (Exception Ex) + { + Log.Warning("Failed to remove old directory {0}. {1}", SubDir.FullName, Ex.Message); + } + } + else + { + CleanupMarkedDirectories(SubDir.FullName, Days); + } + } + } } public class Image diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Utils/Gauntlet.HtmlBuilder.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Utils/Gauntlet.HtmlBuilder.cs new file mode 100644 index 000000000000..ffaa9cd62ef4 --- /dev/null +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Framework/Utils/Gauntlet.HtmlBuilder.cs @@ -0,0 +1,362 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Gauntlet +{ + public class HtmlBuilder + { + + protected StringBuilder SB; + protected bool CurrentlyBuildingTable = false; + /// + /// All of the possible ways to modify a stock table cell. + /// + public class TableCellInfo + { + public bool WithBorder = true; + public bool CenterAlign = true; + /// + /// Number of columns the cell spans. + /// + public int CellColumnWidth = 1; + public string BackgroundColor = "#ffffff"; + public string BorderType = "border:1px solid black;"; + public string TableContents; + public void ResetToDefaults() + { + CellColumnWidth = 1; + BackgroundColor = "#ffffff"; + BorderType = "border:1px solid black;"; + CenterAlign = true; + WithBorder = true; + } + } + + /// + /// All of the possible ways to modify a basic string via formatting. + /// + public class TextOptions + { + public bool Italics = false; + public bool Bold = false; + public string TextColorOverride = string.Empty; + public string Hyperlink = string.Empty; + public string ApplyToString(string TextToAlter) + { + string OutString = TextToAlter; + if (Italics) + { + OutString = string.Format("{0}", OutString); + } + if (Bold) + { + OutString = string.Format("{0}", OutString); + } + if (!string.IsNullOrEmpty(TextColorOverride)) + { + OutString = string.Format("{1}", TextColorOverride, OutString); + } + if (!string.IsNullOrEmpty(Hyperlink)) + { + OutString = string.Format("{1}", Hyperlink, OutString); + } + return OutString; + } + + } + + public HtmlBuilder() + { + SB = new StringBuilder(); + } + + + /// + /// Returns our formatted text + /// + /// + public override string ToString() + { + return SB.ToString(); + } + + /// + /// Returns true if the correct body ends with a new line + /// + public bool EndsWithNewLine + { + get { return SB.Length == 0 || SB.ToString().EndsWith("
") ; } + } + + /// + /// Ensures any text after this starts on a new line + /// + /// + public HtmlBuilder EnsureEndsWithNewLine() + { + if (SB.Length > 0 && !EndsWithNewLine) + { + NewLine(); + } + return this; + } + + /// + /// Returns true if the correct body ends with a new line + /// + public bool EndsWithRowClose + { + get { return SB.Length == 0 || SB.ToString().EndsWith(""); } + } + + /// + /// Ensures any text after this starts on a new line + /// + /// + public HtmlBuilder EnsureEndsWithRowClose() + { + if (SB.Length > 0 && !EndsWithNewLine) + { + SB.Append(""); + } + return this; + } + + /// + /// Open up a new table, if one doesn't already exist, to start adding cells to. + /// + /// + public HtmlBuilder StartBuildingTable() + { + if (!CurrentlyBuildingTable) + { + EnsureEndsWithNewLine(); + SB.Append(""); + CurrentlyBuildingTable = true; + } + return this; + } + + /// + /// Open a new row. Close any existing open rows if they already exist. + /// + /// + public HtmlBuilder StartRow() + { + EnsureEndsWithRowClose(); + SB.Append(""); + return this; + } + /// + /// Finalize a row with the proper tags. + /// + /// + public HtmlBuilder EndRow() + { + SB.Append(""); + return this; + } + + protected string CreateCell(string TableContents, TableCellInfo CellInfo) + { + + return string.Format("", + CellInfo.BackgroundColor, + CellInfo.CellColumnWidth, + CellInfo.CenterAlign ? "align=center" : "", + CellInfo.WithBorder ? string.Format(" style=\"{0}\"", CellInfo.BorderType) : "", + TableContents); + } + /// + /// Add a new cell with any formatting applied. + /// + /// + /// + /// + public HtmlBuilder AddNewCell(string TableContents, TableCellInfo CellInfo = null) + { + SB.Append(CreateCell(TableContents, CellInfo != null ? CellInfo : new TableCellInfo())); + return this; + } + + /// + /// Finish up an existing table. Close off any existing rows before closing the table and newlining. + /// + /// + public HtmlBuilder FinalizeTable() + { + if (CurrentlyBuildingTable) + { + EnsureEndsWithRowClose(); + SB.Append("
{4}

"); + CurrentlyBuildingTable = false; + } + return this; + } + + public HtmlBuilder NewLine() + { + SB.Append("
"); + return this; + } + + /// + /// Insert a hyperlink + /// + /// + /// + public HtmlBuilder Hyperlink(string URL, string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("{1}", Text); + return this; + } + + /// + ///Insert an H1 Header + /// + /// + /// + public HtmlBuilder H1(string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("

{0}

", Text); + return this; + } + + /// + /// Insert an H2 header + /// + /// + /// + public HtmlBuilder H2(string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("

{0}

", Text); + return this; + } + + /// + /// Insert an H3 header + /// + /// + /// + public HtmlBuilder H3(string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("

{0}

", Text); + return this; + } + + /// + /// Insert an H4 Header + /// + /// + /// + public HtmlBuilder H4(string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("

{0}

", Text); + return this; + } + /// + /// Insert an H5 header + /// + /// + /// + public HtmlBuilder H5(string Text) + { + EnsureEndsWithNewLine(); + SB.AppendFormat("
{0}
", Text); + return this; + } + + /// + /// Insert an ordered (numbered) list + /// + /// + /// + public HtmlBuilder OrderedList(IEnumerable Items) + { + EnsureEndsWithNewLine(); + SB.Append("
    "); + foreach (string Item in Items) + { + SB.AppendFormat("
  1. {0}
  2. ", Item); + SB.AppendLine(); + } + SB.Append("
"); + NewLine(); + + return this; + } + + /// + /// Insert an unordered (bulleted) list + /// + /// + /// + public HtmlBuilder UnorderedList(IEnumerable Items) + { + EnsureEndsWithNewLine(); + SB.Append("
    "); + foreach (string Item in Items) + { + SB.AppendFormat("
  • {0}
  • ", Item); + SB.AppendLine(); + } + SB.Append("
"); + NewLine(); + + return this; + } + + /// + /// Append the provided Markdown to our body + /// + /// + /// + public HtmlBuilder Append(HtmlBuilder RHS) + { + SB.Append(RHS.ToString()); + return this; + } + + /// + /// Append the provided text to our body. No formatting will be applied + /// + /// + /// + public HtmlBuilder Append(string RHS) + { + SB.Append(RHS); + return this; + } + + + + /// + /// Append the provided text to our body. No formatting will be applied + /// + /// + /// + public HtmlBuilder AppendFormatted(string RHS, TextOptions InOptions) + { + if (InOptions == null) + { + SB.Append(RHS); + } + else + { + SB.Append(InOptions.ApplyToString(RHS)); + } + return this; + } + + } +} diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Gauntlet.Automation.csproj b/Engine/Source/Programs/AutomationTool/Gauntlet/Gauntlet.Automation.csproj index bef6ee21c763..37c476e53f6b 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Gauntlet.Automation.csproj +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Gauntlet.Automation.csproj @@ -83,7 +83,9 @@ + + True @@ -121,6 +123,7 @@ + @@ -207,6 +210,14 @@ + + + + + + + + @@ -224,6 +235,7 @@ + @@ -232,6 +244,7 @@ + @@ -251,6 +264,7 @@ + diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.IOSBuildSource.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.IOSBuildSource.cs new file mode 100644 index 000000000000..372c0c9c1fdf --- /dev/null +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.IOSBuildSource.cs @@ -0,0 +1,251 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using AutomationTool; +using UnrealBuildTool; +using System.Threading; +using System.Text.RegularExpressions; +using System.Linq; + +namespace Gauntlet +{ + public class IOSBuild : IBuild + { + public UnrealTargetConfiguration Configuration { get; protected set; } + + public string SourceIPAPath; + + public Dictionary FilesToInstall; + + public string PackageName; + + public BuildFlags Flags { get; protected set; } + + public UnrealTargetPlatform Platform { get { return UnrealTargetPlatform.IOS; } } + + public IOSBuild(UnrealTargetConfiguration InConfig, string InPackageName, string InIPAPath, Dictionary InFilesToInstall, BuildFlags InFlags) + { + Configuration = InConfig; + PackageName = InPackageName; + SourceIPAPath = InIPAPath; + FilesToInstall = InFilesToInstall; + Flags = InFlags; + } + + public bool CanSupportRole(UnrealTargetRole RoleType) + { + if (RoleType.IsClient()) + { + return true; + } + + return false; + } + + public static IProcessResult ExecuteCommand(String Command, String Arguments) + { + CommandUtils.ERunOptions RunOptions = CommandUtils.ERunOptions.AppMustExist; + + if (Log.IsVeryVerbose) + { + RunOptions |= CommandUtils.ERunOptions.AllowSpew; + } + else + { + RunOptions |= CommandUtils.ERunOptions.NoLoggingOfRunCommand; + } + + Log.Verbose("Executing '{0} {1}'", Command, Arguments); + + IProcessResult Result = CommandUtils.Run(Command, Arguments, Options: RunOptions); + + return Result; + } + + private static string GetBundleIdentifier(string SourceIPA) + { + // Get a list of files in the IPA + IProcessResult Result = ExecuteCommand("unzip", String.Format("-Z1 {0}", SourceIPA)); + + if (Result.ExitCode != 0) + { + Log.Warning(String.Format("Unable to list files for IPA {0}", SourceIPA)); + return null; + } + + string[] Filenames = Regex.Split(Result.Output, "\r\n|\r|\n"); + string PList = Filenames.Where(F => F.ToLower().Contains("info.plist")).FirstOrDefault(); + + if (String.IsNullOrEmpty(PList)) + { + Log.Warning(String.Format("Unable to find plist for IPA {0}", SourceIPA)); + return null; + } + + // Get the plist info + Result = ExecuteCommand("unzip", String.Format("-p '{0}' '{1}'", SourceIPA, PList)); + + if (Result.ExitCode != 0) + { + Log.Warning(String.Format("Unable to extract plist data for IPA {0}", SourceIPA)); + return null; + } + + string PlistInfo = Result.Output; + + // todo: plist parsing, could be better + string PackageName = null; + string KeyString = "CFBundleIdentifier"; + int KeyIndex = PlistInfo.IndexOf(KeyString); + if (KeyIndex > 0) + { + int StartIdx = PlistInfo.IndexOf("", KeyIndex + KeyString.Length) + "".Length; + int EndIdx = PlistInfo.IndexOf("", StartIdx); + if (StartIdx > 0 && EndIdx > StartIdx) + { + PackageName = PlistInfo.Substring(StartIdx, EndIdx - StartIdx); + } + } + + if (String.IsNullOrEmpty(PackageName)) + { + Log.Warning(String.Format("Unable to find CFBundleIdentifier in plist info for IPA {0}", SourceIPA)); + return null; + } + + Log.Verbose("Found bundle id: {0}", PackageName); + + return PackageName; + + } + + public static IEnumerable CreateFromPath(string InProjectName, string InPath) + { + string BuildPath = InPath; + + List DiscoveredBuilds = new List(); + + DirectoryInfo Di = new DirectoryInfo(BuildPath); + + // find all install batchfiles + FileInfo[] InstallFiles = Di.GetFiles("*.ipa"); + + foreach (FileInfo Fi in InstallFiles) + { + + var UnrealConfig = UnrealHelpers.GetConfigurationFromExecutableName(InProjectName, Fi.Name); + + Log.Verbose("Pulling package data from {0}", Fi.FullName); + + string AbsPath = Fi.Directory.FullName; + + // IOS builds are always packaged, and we can always replace the command line + BuildFlags Flags = BuildFlags.Packaged | BuildFlags.CanReplaceCommandLine; + + if (AbsPath.Contains("Bulk")) + { + Flags |= BuildFlags.Bulk; + } + + string SourceIPAPath = Fi.FullName; + string PackageName = GetBundleIdentifier(SourceIPAPath); + + if (String.IsNullOrEmpty(PackageName)) + { + continue; + } + + Dictionary FilesToInstall = new Dictionary(); + + IOSBuild NewBuild = new IOSBuild(UnrealConfig, PackageName, SourceIPAPath, FilesToInstall, Flags); + + DiscoveredBuilds.Add(NewBuild); + + Log.Verbose("Found {0} {1} build at {2}", UnrealConfig, ((Flags & BuildFlags.Bulk) == BuildFlags.Bulk) ? "(bulk)" : "(not bulk)", AbsPath); + + } + + return DiscoveredBuilds; + } + } + + public class IOSBuildSource : IFolderBuildSource + { + public string BuildName { get { return "IOSBuildSource"; } } + + public bool CanSupportPlatform(UnrealTargetPlatform InPlatform) + { + return InPlatform == UnrealTargetPlatform.IOS; + } + + public string ProjectName { get; protected set; } + + public IOSBuildSource() + { + } + + public List GetBuildsAtPath(string InProjectName, string InPath, int MaxRecursion = 3) + { + // We only want iOS builds on Mac host + if (BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) + { + return new List(); + } + + List AllDirs = new List(); + + // c:\path\to\build + DirectoryInfo PathDI = new DirectoryInfo(InPath); + + if (PathDI.Name.IndexOf("IOS", StringComparison.OrdinalIgnoreCase) >= 0) + { + AllDirs.Add(PathDI); + } + + // find all directories that begin with IOS + DirectoryInfo[] IOSDirs = PathDI.GetDirectories("IOS*", SearchOption.TopDirectoryOnly); + + AllDirs.AddRange(IOSDirs); + + List DirsToRecurse = AllDirs; + + // now get subdirs + while (MaxRecursion-- > 0) + { + List DiscoveredDirs = new List(); + + DirsToRecurse.ToList().ForEach((D) => + { + DiscoveredDirs.AddRange(D.GetDirectories("*", SearchOption.TopDirectoryOnly)); + }); + + AllDirs.AddRange(DiscoveredDirs); + DirsToRecurse = DiscoveredDirs; + } + + //IOSBuildSource BuildSource = null; + List Builds = new List(); + + string IOSBuildFilter = Globals.Params.ParseValue("IOSBuildFilter", ""); + foreach (DirectoryInfo Di in AllDirs) + { + IEnumerable FoundBuilds = IOSBuild.CreateFromPath(InProjectName, Di.FullName); + + if (FoundBuilds != null) + { + if (!string.IsNullOrEmpty(IOSBuildFilter)) + { + //IndexOf used because Contains must be case-sensitive + FoundBuilds = FoundBuilds.Where(B => B.SourceIPAPath.IndexOf(IOSBuildFilter, StringComparison.OrdinalIgnoreCase) >= 0); + } + Builds.AddRange(FoundBuilds); + } + } + + return Builds; + } + + } +} \ No newline at end of file diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.TargetDeviceIOS.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.TargetDeviceIOS.cs new file mode 100644 index 000000000000..60402adb2443 --- /dev/null +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/IOS/Gauntlet.TargetDeviceIOS.cs @@ -0,0 +1,985 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Linq; +using System.Security.Cryptography; +using AutomationTool; +using UnrealBuildTool; +using System.Text; +using System.Text.RegularExpressions; + +/* + +General Device Notes (and areas for improvement): + +1) We don't currently support parallel iOS tests, see https://jira.it.epicgames.net/browse/UEATM-219 +2) Device Farm devices should be in airplane mode + wifi to avoid No Sim warning notification + +*/ + +namespace Gauntlet +{ + + class IOSAppInstance : IAppInstance + { + protected IOSAppInstall Install; + public IOSAppInstance(IOSAppInstall InInstall, IProcessResult InProcess, string InCommandLine) + { + Install = InInstall; + this.CommandLine = InCommandLine; + this.ProcessResult = InProcess; + } + + public string ArtifactPath + { + get + { + if (bHaveSavedArtifacts == false) + { + if (HasExited) + { + SaveArtifacts(); + bHaveSavedArtifacts = true; + } + } + + return Install.IOSDevice.LocalCachePath + "/" + Install.IOSDevice.DeviceArtifactPath; + } + } + + public ITargetDevice Device + { + get + { + return Install.Device; + } + } + + protected void SaveArtifacts() + { + TargetDeviceIOS Device = Install.IOSDevice; + + // copy remote artifacts to local + string CommandLine = String.Format("--bundle_id {0} --download={1} --to {2}", Install.PackageName, Device.DeviceArtifactPath, Device.LocalCachePath); + + IProcessResult DownloadCmd = Device.ExecuteIOSDeployCommand(CommandLine, 120); + + if (DownloadCmd.ExitCode != 0) + { + Log.Warning("Failed to retrieve artifacts. {0}", DownloadCmd.Output); + } + + } + + public IProcessResult ProcessResult { get; private set; } + + public bool HasExited { get { return ProcessResult.HasExited; } } + + public bool WasKilled { get; protected set; } + + public int ExitCode { get { return ProcessResult.ExitCode; } } + + public string CommandLine { get; private set; } + + public string StdOut + { + get + { + if (HasExited) + { + // The ios application is being run under lldb by ios-deploy + // lldb catches crashes and we have it setup to dump thread callstacks + // parse any crash dumps into Unreal crash format and append to output + string CrashLog = LLDBCrashParser.GenerateCrashLog(ProcessResult.Output); + + if (!string.IsNullOrEmpty(CrashLog)) + { + return String.Format("{0}\n{1}", ProcessResult.Output, CrashLog); + } + } + + return ProcessResult.Output; + + } + } + + public int WaitForExit() + { + if (!HasExited) + { + ProcessResult.WaitForExit(); + } + + return ExitCode; + } + + public void Kill() + { + if (!HasExited) + { + WasKilled = true; + ProcessResult.ProcessObject.Kill(); + } + } + + + internal bool bHaveSavedArtifacts; + } + + + class IOSAppInstall : IAppInstall + { + public string Name { get; protected set; } + + public string CommandLine { get; protected set; } + + public string PackageName { get; protected set; } + + public ITargetDevice Device { get { return IOSDevice; } } + + public TargetDeviceIOS IOSDevice; + + public IOSAppInstall(string InName, TargetDeviceIOS InDevice, string InPackageName, string InCommandLine) + { + Name = InName; + CommandLine = InCommandLine; + PackageName = InPackageName; + + IOSDevice = InDevice; + } + + public IAppInstance Run() + { + return Device.Run(this); + } + } + + public class IOSDeviceFactory : IDeviceFactory + { + public bool CanSupportPlatform(UnrealTargetPlatform Platform) + { + return Platform == UnrealTargetPlatform.IOS; + } + + public ITargetDevice CreateDevice(string InRef, string InParam) + { + return new TargetDeviceIOS(InRef); + } + } + + /// + /// iOS implementation of a device to run applications + /// + public class TargetDeviceIOS : ITargetDevice + { + public string Name { get; protected set; } + + /// + /// Low-level device name (uuid) + /// + public string DeviceName { get; protected set; } + + protected Dictionary LocalDirectoryMappings { get; set; } + + public TargetDeviceIOS(string InName) + { + IsDefaultDevice = (String.IsNullOrEmpty(InName) || InName.Equals("default", StringComparison.OrdinalIgnoreCase)); + + Name = InName; + LocalDirectoryMappings = new Dictionary(); + + var DefaultDevices = GetConnectedDeviceUUID(); + + // If no device name or its 'default' then use the first default device + if (IsDefaultDevice) + { + if (DefaultDevices.Count() == 0) + { + throw new AutomationException("No default device available"); + } + + DeviceName = DefaultDevices.First(); + + Log.Verbose("Selected device {0} as default", DeviceName); + } + else + { + DeviceName = InName.Trim(); + if (!DefaultDevices.Contains(DeviceName)) + { + throw new AutomationException("Device with UUID {0} not found in device list", DeviceName); + } + } + + // setup local cache + LocalCachePath = Path.Combine(GauntletAppCache, "Device_" + Name); + if (Directory.Exists(LocalCachePath)) + { + Directory.Delete(LocalCachePath, true); + } + + Directory.CreateDirectory(LocalCachePath); + } + + bool IsDefaultDevice = false; + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + try + { + if (Directory.Exists(LocalCachePath)) + { + Directory.Delete(LocalCachePath, true); + } + } + catch (Exception Ex) + { + Log.Warning("TargetDeviceIOS.Dispose() threw: {0}", Ex.Message); + } + finally + { + disposedValue = true; + } + + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + + public CommandUtils.ERunOptions RunOptions { get; set; } + + public IAppInstance Run(IAppInstall App) + { + IOSAppInstall IOSApp = App as IOSAppInstall; + + if (IOSApp == null) + { + throw new DeviceException("AppInstance is of incorrect type!"); + } + + string CommandLine = IOSApp.CommandLine.Replace("\"", "\\\""); + + Log.Info("Launching {0} on {1}", App.Name, ToString()); + Log.Verbose("\t{0}", CommandLine); + + // ios-deploy notes: -L launches detached, -I non-interactive (exits when app exits), -r uninstalls before install (removes app Documents folder) + // -t number of seconds to wait for device to be connected + + // setup symbols if available + string DSymBundle = ""; + string DSymDir = Path.Combine(GauntletAppCache, "Symbols"); + + if (Directory.Exists(DSymDir)) + { + DSymBundle = Directory.GetDirectories(DSymDir).Where(D => Path.GetExtension(D).ToLower() == ".dsym").FirstOrDefault(); + DSymBundle = string.IsNullOrEmpty(DSymBundle) ? "" : DSymBundle = " -s \"" + DSymBundle + "\""; + } + + string CL = "--noinstall -I" + DSymBundle + " -b \"" + LocalAppBundle + "\" --args '" + CommandLine.Trim() + "'"; + + IProcessResult Result = ExecuteIOSDeployCommand(CL, 0); + + Thread.Sleep(5000); + + // Give ios-deploy a chance to throw out any errors... + if (Result.HasExited) + { + Log.Warning("ios-deploy exited early: " + Result.Output); + throw new DeviceException("Failed to launch on {0}. {1}", Name, Result.Output); + } + + return new IOSAppInstance(IOSApp, Result, IOSApp.CommandLine); + } + + /// + /// Remove the application entirely from the iOS device, this includes any persistent app data in /Documents + /// + private void RemoveApplication(IOSBuild Build) + { + string CommandLine = String.Format("--bundle_id {0} --uninstall_only", Build.PackageName); + ExecuteIOSDeployCommand(CommandLine); + } + + /// + /// Remove artifacts from device + /// + private bool CleanDeviceArtifacts(IOSBuild Build) + { + try + { + Log.Verbose("Cleaning device artifacts"); + + string CleanCommand = String.Format("--bundle_id {0} --rm_r {1}", Build.PackageName, DeviceArtifactPath); + IProcessResult Result = ExecuteIOSDeployCommand(CleanCommand, 120); + + if (Result.ExitCode != 0) + { + Log.Warning("Failed to clean artifacts from device"); + return false; + } + + } + catch (Exception Ex) + { + Log.Verbose("Exception while cleaning artifacts from device: {0}", Ex.Message); + } + + return true; + + } + + /// + /// Checks whether version of deployed bundle matches local IPA + /// + bool CheckDeployedIPA(IOSBuild Build) + { + try + { + Log.Verbose("Checking deployed IPA hash"); + + string CommandLine = String.Format("--bundle_id {0} --download={1} --to {2}", Build.PackageName, "/Documents/IPAHash.txt", LocalCachePath); + IProcessResult Result = ExecuteIOSDeployCommand(CommandLine, 120); + + if (Result.ExitCode != 0) + { + return false; + } + + string Hash = File.ReadAllText(LocalCachePath + "/Documents/IPAHash.txt").Trim(); + string StoredHash = File.ReadAllText(IPAHashFilename).Trim(); + + if (Hash == StoredHash) + { + Log.Verbose("Deployed app hash matched cached IPA hash"); + return true; + } + + } + catch (Exception Ex) + { + if (!Ex.Message.Contains("is denied")) + { + Log.Verbose("Unable to pull cached IPA cache from device, cached file may not exist: {0}", Ex.Message); + } + } + + Log.Verbose("Deployed app hash doesn't match, IPA will be installed"); + return false; + + } + + public IAppInstall InstallApplication(UnrealAppConfig AppConfig) + { + IOSBuild Build = AppConfig.Build as IOSBuild; + + // Ensure Build exists + if (Build == null) + { + throw new AutomationException("Invalid build for IOS!"); + } + + Log.Info("Installing using IPA {0}", Build.SourceIPAPath); + + // device artifact path + DeviceArtifactPath = string.Format("/Documents/{0}/Saved", AppConfig.ProjectName); + + PrepareIPA(Build); + + if (!CheckDeployedIPA(Build)) + { + // uninstall will clean all device artifacts + ExecuteIOSDeployCommand(String.Format("--uninstall -b \"{0}\"", LocalAppBundle), 10 * 60); + } + else + { + // remove device artifacts + CleanDeviceArtifacts(Build); + } + + // parallel iOS tests use same app install folder, so lock it as setup is quick + lock (Globals.MainLock) + { + // local app install with additional files, this directory will be mirrored to device in a single operation + string AppInstallPath; + + AppInstallPath = Path.Combine(Globals.TempDir, "iOSAppInstall"); + + if (Directory.Exists(AppInstallPath)) + { + Directory.Delete(AppInstallPath, true); + } + + Directory.CreateDirectory(AppInstallPath); + + if (LocalDirectoryMappings.Count == 0) + { + PopulateDirectoryMappings(AppInstallPath); + } + + //@todo: Combine Build and AppConfig files, this should be done in higher level code, not per device implementation + + if (AppConfig.FilesToCopy != null) + { + foreach (UnrealFileToCopy FileToCopy in AppConfig.FilesToCopy) + { + string PathToCopyTo = Path.Combine(LocalDirectoryMappings[FileToCopy.TargetBaseDirectory], FileToCopy.TargetRelativeLocation); + + if (File.Exists(FileToCopy.SourceFileLocation)) + { + FileInfo SrcInfo = new FileInfo(FileToCopy.SourceFileLocation); + SrcInfo.IsReadOnly = false; + string DirectoryToCopyTo = Path.GetDirectoryName(PathToCopyTo); + if (!Directory.Exists(DirectoryToCopyTo)) + { + Directory.CreateDirectory(DirectoryToCopyTo); + } + if (File.Exists(PathToCopyTo)) + { + FileInfo ExistingFile = new FileInfo(PathToCopyTo); + ExistingFile.IsReadOnly = false; + } + + SrcInfo.CopyTo(PathToCopyTo, true); + Log.Verbose("Copying app install: {0} to {1}", FileToCopy, DirectoryToCopyTo); + } + else + { + Log.Warning("File to copy {0} not found", FileToCopy); + } + } + } + + // copy mapped files in a single pass + string CopyCommand = String.Format("--bundle_id {0} --upload={1} --to {2}", Build.PackageName, AppInstallPath, DeviceArtifactPath); + ExecuteIOSDeployCommand(CopyCommand, 120); + + // store the IPA hash to avoid redundant deployments + CopyCommand = String.Format("--bundle_id {0} --upload={1} --to {2}", Build.PackageName, IPAHashFilename, "/Documents/IPAHash.txt"); + ExecuteIOSDeployCommand(CopyCommand, 120); + } + + IOSAppInstall IOSApp = new IOSAppInstall(AppConfig.Name, this, Build.PackageName, AppConfig.CommandLine); + return IOSApp; + } + + public void PopulateDirectoryMappings(string ProjectDir) + { + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Binaries, Path.Combine(ProjectDir, "Binaries")); + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Config, Path.Combine(ProjectDir, "Config")); + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Content, Path.Combine(ProjectDir, "Content")); + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Demos, Path.Combine(ProjectDir, "Demos")); + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Profiling, Path.Combine(ProjectDir, "Profiling")); + LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Saved, ProjectDir); + } + + + public UnrealTargetPlatform Platform { get { return UnrealTargetPlatform.IOS; } } + + /// + /// Temp path we use to push/pull things from the device + /// + public string LocalCachePath { get; protected set; } + + + /// + /// Artifact (e.g. Saved) path on the device + /// + public string DeviceArtifactPath { get; protected set; } + + + #region Device State Management + + // NOTE: We check that a default device UUID or the one specifed is connected with 'ios-deploy --detect' at device creation time + // otherwise, ios-deploy doesn't currently support this style of queries + // It might be possible to add additional lldb queries/commands through the python interface (there are various solutions for reboot, though the ones I have found require jailbreaking) + public bool IsAvailable { get { return true; } } + public bool IsConnected { get { return Connected; } } + public bool IsOn { get { return true; } } + public bool PowerOn() { return true; } + public bool PowerOff() { return true; } + public bool Reboot() { return true; } + + // catch attempts to connect multiple iOS devices in parallel, see https://jira.it.epicgames.net/browse/UEATM-219 + static int ConnectedDeviceCount = 0; + bool Connected = false; + + public bool Connect() + { + lock (Globals.MainLock) + { + if (Connected) + { + return true; + } + + ConnectedDeviceCount++; + + if (ConnectedDeviceCount > 1) + { + + throw new AutomationException("Parallel iOS device connections are not currently supported, see https://jira.it.epicgames.net/browse/UEATM-219"); + } + + Connected = true; + + // Get rid of any zombies, this needs to be reworked to use tracked process id's when running parallel tests + // may need to kill by processid for spawned children + IOSBuild.ExecuteCommand("killall", "ios-deploy"); + Thread.Sleep(2500); + IOSBuild.ExecuteCommand("killall", "lldb"); + Thread.Sleep(2500); + } + + return true; + } + public bool Disconnect() + { + lock (Globals.MainLock) + { + if (!Connected) + { + return true; + } + + Connected = false; + + ConnectedDeviceCount--; + + if (ConnectedDeviceCount < 0) + { + throw new AutomationException("iOS device connection mismatch"); + } + } + + return true; + } + + #endregion + + public override string ToString() + { + return Name; + } + + + /// + /// Get UUID of all connected iOS devices + /// + List GetConnectedDeviceUUID() + { + var Result = ExecuteIOSDeployCommand("--detect"); + + if (Result.ExitCode != 0) + { + return new List(); + } + + MatchCollection DeviceMatches = Regex.Matches(Result.Output, @"(.?)Found\ ([a-z0-9]{40})"); + + return DeviceMatches.Cast().Select( + M => M.Groups[2].ToString() + ).ToList(); + } + + // Gauntlet cache folder for tracking device/ipa state + string GauntletAppCache { get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".gauntletappcache");} } + + // path to locally extracted (and possibly resigned) app bundle + // Note: ios-deploy works with app bundles, which requires the IPA be unzipped for deployment (this will allow us to resign in the future as well) + string LocalAppBundle = null; + + // the current IPA MD5 hash, which is tracked to avoid unneccessary deployments and unzip operations + string IPAHashFilename { get { return Path.Combine(GauntletAppCache, "IPAHash.txt"); } } + + /// + /// Generate MD5 and cache IPA bundle files + /// + private bool PrepareIPA(IOSBuild Build) + { + Log.Info("Preparing IPA {0}", Build.SourceIPAPath); + + try + { + // cache the unzipped app using a MD5 checksum, avoiding needing to unzip + string Hash = null; + string StoredHash = null; + using (var MD5Hash = MD5.Create()) + { + using (var Stream = File.OpenRead(Build.SourceIPAPath)) + { + Hash = BitConverter.ToString(MD5Hash.ComputeHash(Stream)).Replace("-", "").ToLowerInvariant(); + } + } + string PayloadDir = Path.Combine(GauntletAppCache, "Payload"); + string SymbolsDir = Path.Combine(GauntletAppCache, "Symbols"); + + if (File.Exists(IPAHashFilename) && Directory.Exists(PayloadDir)) + { + StoredHash = File.ReadAllText(IPAHashFilename).Trim(); + if (Hash != StoredHash) + { + Log.Verbose("IPA hash out of date, clearing cache"); + StoredHash = null; + } + } + + if (String.IsNullOrEmpty(StoredHash) || Hash != StoredHash) + { + if (Directory.Exists(PayloadDir)) + { + Directory.Delete(PayloadDir, true); + } + + if (Directory.Exists(SymbolsDir)) + { + Directory.Delete(SymbolsDir, true); + } + + Log.Verbose("Unzipping IPA {0} to cache at: {1}", Build.SourceIPAPath, GauntletAppCache); + + IProcessResult Result = IOSBuild.ExecuteCommand("unzip", String.Format("{0} -d {1}", Build.SourceIPAPath, GauntletAppCache)); + if (Result.ExitCode != 0 || !Directory.Exists(PayloadDir)) + { + throw new Exception(String.Format("Unable to extract IPA {0}", Build.SourceIPAPath)); + } + + // Cache symbols for symbolicated callstacks + string SymbolsZipFile = string.Format("{0}/../../Symbols/{1}.dSYM.zip", Path.GetDirectoryName(Build.SourceIPAPath), Path.GetFileNameWithoutExtension(Build.SourceIPAPath)); + + Log.Verbose("Checking Symbols at {0}", SymbolsZipFile); + + if (File.Exists(SymbolsZipFile)) + { + Log.Verbose("Unzipping Symbols {0} to cache at: {1}", SymbolsZipFile, SymbolsDir); + + Result = IOSBuild.ExecuteCommand("unzip", String.Format("{0} -d {1}", SymbolsZipFile, SymbolsDir)); + if (Result.ExitCode != 0 || !Directory.Exists(SymbolsDir)) + { + throw new Exception(String.Format("Unable to extract build symbols {0} -> {1}", SymbolsZipFile, SymbolsDir)); + } + } + + // store hash + File.WriteAllText(IPAHashFilename, Hash); + + Log.Verbose("IPA cached"); + } + else + { + Log.Verbose("Using cached IPA"); + } + + LocalAppBundle = Directory.GetDirectories(PayloadDir).Where(D => Path.GetExtension(D) == ".app").FirstOrDefault(); + + if (String.IsNullOrEmpty(LocalAppBundle)) + { + throw new Exception(String.Format("Unable to find app in local app bundle {0}", PayloadDir)); + } + + } + catch (Exception Ex) + { + throw new AutomationException("Unable to prepare {0} : {1}", Build.SourceIPAPath, Ex.Message); + } + + return true; + } + + public Dictionary GetPlatformDirectoryMappings() + { + if (LocalDirectoryMappings.Count == 0) + { + Log.Warning("Platform directory mappings have not been populated for this platform! This should be done within InstallApplication()"); + } + return LocalDirectoryMappings; + } + + public IProcessResult ExecuteIOSDeployCommand(String CommandLine, int WaitTime = 60, bool WarnOnTimeout = true) + { + if (!IsDefaultDevice) + { + CommandLine = String.Format("--id {0} {1}", DeviceName, CommandLine); + } + + String IOSDeployPath = Path.Combine(Globals.UE4RootDir, "Engine/Extras/ThirdPartyNotUE/ios-deploy/bin/ios-deploy"); + + if (!File.Exists(IOSDeployPath)) + { + throw new AutomationException("Unable to run ios-deploy binary at {0}", IOSDeployPath); + } + + CommandUtils.ERunOptions RunOptions = CommandUtils.ERunOptions.NoWaitForExit; + + if (Log.IsVeryVerbose) + { + RunOptions |= CommandUtils.ERunOptions.AllowSpew; + } + else + { + RunOptions |= CommandUtils.ERunOptions.NoLoggingOfRunCommand; + } + + Log.Verbose("ios-deploy executing '{0}'", CommandLine); + + IProcessResult Result = CommandUtils.Run(IOSDeployPath, CommandLine, Options: RunOptions); + + if (WaitTime > 0) + { + DateTime StartTime = DateTime.Now; + + Result.ProcessObject.WaitForExit(WaitTime * 1000); + + if (Result.HasExited == false) + { + if ((DateTime.Now - StartTime).TotalSeconds >= WaitTime) + { + string Message = String.Format("IOSDeployPath timeout after {0} secs: {1}, killing process", WaitTime, CommandLine); + + if (WarnOnTimeout) + { + Log.Warning(Message); + } + else + { + Log.Info(Message); + } + + Result.ProcessObject.Kill(); + // wait up to 15 seconds for process exit + Result.ProcessObject.WaitForExit(15000); + } + } + } + + return Result; + } + + } + + + /// + /// Helper class to parses LLDB crash threads and generate Unreal compatible log callstack + /// + static class LLDBCrashParser + { + // Frame in callstack + class FrameInfo + { + public string Module; + public string Symbol; + public string Address; + public string Offset; + public string Source; + public string Line; + + public override string ToString() + { + // symbolicated + if (!String.IsNullOrEmpty(Source)) + { + return string.Format("Error: [Callstack] 0x{0} {1}!{2} [{3}{4}]", Address, Module, Symbol.Replace(" ", "^"), Source, String.IsNullOrEmpty(Line) ? "" : ":" + Line); + } + + // unsymbolicated + return string.Format("Error: [Callstack] 0x{0} {1}!{2} [???]", Address, Module, Symbol.Replace(" ", "^")); + } + + } + + // Parsed thread callstack + class ThreadInfo + { + public int Num; + public string Status; + public bool Current; + public List Frames = new List(); + + public override string ToString() + { + return string.Format("{0}{1}{2}\n{3}", Num, string.IsNullOrEmpty(Status) ? "" : " " + Status + " ", Current ? " (Current)" : "", string.Join("\n", Frames)); + } + } + + /// + /// Parse lldb thread crash dump to Unreal log format + /// + public static string GenerateCrashLog(string LogOutput) + { + DateTime TimeStamp; + int Frame; + ThreadInfo Thread = ParseCallstack(LogOutput, out TimeStamp, out Frame); + if (Thread == null) + { + return null; + } + + StringBuilder CrashLog = new StringBuilder(); + CrashLog.Append(string.Format("[{0}:000][{1}]LogCore: === Fatal Error: ===\n", TimeStamp.ToString("yyyy.mm.dd - H.mm.ss"), Frame)); + CrashLog.Append(string.Format("Error: Thread #{0} {1}\n", Thread.Num, Thread.Status)); + CrashLog.Append(string.Join("\n", Thread.Frames)); + + return CrashLog.ToString(); + + } + + static ThreadInfo ParseCallstack(string LogOutput, out DateTime Timestamp, out int FrameNum) + { + Timestamp = DateTime.UtcNow; + FrameNum = 0; + + Regex LogLineRegex = new Regex(@"(?\s\[\d.+\]\[\s*\d+\])(?.*)"); + Regex TimeRegex = new Regex(@"\[(?\d+)\.(?\d+)\.(?\d+)-(?\d+)\.(?\d+)\.(?\d+):(?\d+)\]\[(?\s*\d+)\]", RegexOptions.IgnoreCase); + Regex ThreadRegex = new Regex(@"(thread\s#)(?\d+),?(?.+)"); + Regex SymbolicatedFrameRegex = new Regex(@"\s#(?\d+):\s0x(?
[\da-f]+)\s(?.+)\`(?.+)(\sat\s)(?.+)\s\[opt\]"); + Regex UnsymbolicatedFrameRegex = new Regex(@"frame\s#(?\d+):\s0x(?
[\da-f]+)\s(?.+)\`(?.+)\s\+\s(?\d+)"); + + LinkedList CrashLog = new LinkedList(Regex.Split(LogOutput, "\r\n|\r|\n")); + + List Threads = new List(); + ThreadInfo Thread = null; + ThreadInfo CrashThread = null; + + var LineNode = CrashLog.First; + while (LineNode != null) + { + string Line = LineNode.Value.Trim(); + + // If Gauntlet marks the test as complete, ignore any thread dumps from forcing process to exit + if (Line.Contains("**** TEST COMPLETE. EXIT CODE: 0 ****")) + { + return null; + } + + // Parse log timestamps + if (LogLineRegex.IsMatch(Line)) + { + GroupCollection LogGroups = LogLineRegex.Match(Line).Groups; + if (TimeRegex.IsMatch(LogGroups["timestamp"].Value)) + { + GroupCollection TimeGroups = TimeRegex.Match(LogGroups["timestamp"].Value).Groups; + int Year = int.Parse(TimeGroups["year"].Value); + int Month = int.Parse(TimeGroups["month"].Value); + int Day = int.Parse(TimeGroups["day"].Value); + int Hour = int.Parse(TimeGroups["hour"].Value); + int Minute = int.Parse(TimeGroups["minute"].Value); + int Second = int.Parse(TimeGroups["second"].Value); + FrameNum = int.Parse(TimeGroups["frame"].Value); + Timestamp = new DateTime(Year, Month, Day, Hour, Minute, Second); + } + + LineNode = LineNode.Next; + continue; + } + + if (Thread != null) + { + FrameInfo Frame = null; + GroupCollection FrameGroups = null; + + // Parse symbolicated frame + if (SymbolicatedFrameRegex.IsMatch(Line)) + { + FrameGroups = SymbolicatedFrameRegex.Match(Line).Groups; + + Frame = new FrameInfo() + { + Address = FrameGroups["address"].Value, + Module = FrameGroups["module"].Value, + Symbol = FrameGroups["symbol"].Value, + }; + + Frame.Source = FrameGroups["source"].Value; + if (Frame.Source.Contains(":")) + { + Frame.Source = FrameGroups["source"].Value.Split(':')[0]; + Frame.Line = FrameGroups["source"].Value.Split(':')[1]; + } + } + + // Parse unsymbolicated frame + if (UnsymbolicatedFrameRegex.IsMatch(Line)) + { + FrameGroups = UnsymbolicatedFrameRegex.Match(Line).Groups; + + Frame = new FrameInfo() + { + Address = FrameGroups["address"].Value, + Offset = FrameGroups["offset"].Value, + Module = FrameGroups["module"].Value, + Symbol = FrameGroups["symbol"].Value + }; + } + + if (Frame != null) + { + Thread.Frames.Add(Frame); + } + else + { + Thread = null; + } + + } + + // Parse thread + if (ThreadRegex.IsMatch(Line)) + { + GroupCollection ThreadGroups = ThreadRegex.Match(Line).Groups; + Thread = new ThreadInfo() + { + Num = int.Parse(ThreadGroups["threadnum"].Value), + Status = ThreadGroups["status"].Value.Trim() + }; + + if (Line.StartsWith("*")) + { + Thread.Current = true; + } + + if (CrashThread == null) + { + CrashThread = Thread; + } + else + { + Threads.Add(Thread); + } + } + + LineNode = LineNode.Next; + } + + if (CrashThread == null) + { + return null; + } + + Thread = Threads.Single(T => T.Num == CrashThread.Num); + + if (Thread == null) + { + Log.Warning("Unable to parse full crash callstack"); + Thread = CrashThread; + } + + return Thread; + + } + + } +} \ No newline at end of file diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Mac/Gauntlet.TargetDeviceMac.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Mac/Gauntlet.TargetDeviceMac.cs index b0f070cdbfec..5af65c4f4c53 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Mac/Gauntlet.TargetDeviceMac.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Mac/Gauntlet.TargetDeviceMac.cs @@ -186,9 +186,7 @@ namespace Gauntlet Log.Info("Skipping install of {0} (-skipdeploy)", BuildPath); } - // write a token, used to detect and old gauntlet-installedbuilds periodically - string TokenPath = Path.Combine(DestPath, "gauntlet.token"); - File.WriteAllText(TokenPath, "Created by Gauntlet"); + Utils.SystemHelpers.MarkDirectoryForCleanup(DestPath); BuildPath = DestPath; } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Windows/Gauntlet.TargetDeviceWindows.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Windows/Gauntlet.TargetDeviceWindows.cs index 208e74121d9e..52fb29daf1f6 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Windows/Gauntlet.TargetDeviceWindows.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Platform/Windows/Gauntlet.TargetDeviceWindows.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading; using AutomationTool; using UnrealBuildTool; using System.Text.RegularExpressions; @@ -18,17 +20,25 @@ namespace Gauntlet public bool WasKilled { get; protected set; } - public string StdOut { get { return ProcessResult.Output; } } + public string StdOut { get { return string.IsNullOrEmpty(ProcessLogFile) ? ProcessResult.Output : ProcessLogOutput; } } public int ExitCode { get { return ProcessResult.ExitCode; } } public string CommandLine { get; private set; } - public LocalAppProcess(IProcessResult InProcess, string InCommandLine) + public LocalAppProcess(IProcessResult InProcess, string InCommandLine, string InProcessLogFile = null) { this.CommandLine = InCommandLine; this.ProcessResult = InProcess; + this.ProcessLogFile = InProcessLogFile; + + // start reader thread if logging to a file + if (!string.IsNullOrEmpty(InProcessLogFile)) + { + new System.Threading.Thread(LogFileReaderThread).Start(); + } } + public int WaitForExit() { if (!HasExited) @@ -48,18 +58,57 @@ namespace Gauntlet } } + /// + /// Reader thread when logging to file + /// + void LogFileReaderThread() + { + // Wait for the processes log file to be created + while (!File.Exists(ProcessLogFile) && !HasExited) + { + Thread.Sleep(2000); + } + + Thread.Sleep(1000); + + using (FileStream ProcessLog = File.Open(ProcessLogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + StreamReader LogReader = new StreamReader(ProcessLog); + + // Read until the process has exited + do + { + Thread.Sleep(250); + + while (!LogReader.EndOfStream) + { + string Output = LogReader.ReadToEnd(); + + if (Output != null) + { + ProcessLogOutput += Output; + } + } + } + while (!HasExited); + } + } + + public abstract string ArtifactPath { get; } public abstract ITargetDevice Device { get; } - + + string ProcessLogFile; + string ProcessLogOutput = ""; } class WindowsAppInstance : LocalAppProcess { protected WindowsAppInstall Install; - public WindowsAppInstance(WindowsAppInstall InInstall, IProcessResult InProcess) - : base(InProcess, InInstall.CommandArguments) + public WindowsAppInstance(WindowsAppInstall InInstall, IProcessResult InProcess, string ProcessLogFile = null) + : base(InProcess, InInstall.CommandArguments, ProcessLogFile) { Install = InInstall; } @@ -94,15 +143,18 @@ namespace Gauntlet public string ArtifactPath; + public string ProjectName; + public TargetDeviceWindows WinDevice { get; private set; } public ITargetDevice Device { get { return WinDevice; } } public CommandUtils.ERunOptions RunOptions { get; set; } - public WindowsAppInstall(string InName, TargetDeviceWindows InDevice) + public WindowsAppInstall(string InName, string InProjectName, TargetDeviceWindows InDevice) { Name = InName; + ProjectName = InProjectName; WinDevice = InDevice; CommandArguments = ""; this.RunOptions = CommandUtils.ERunOptions.NoWaitForExit; @@ -208,6 +260,7 @@ namespace Gauntlet } IProcessResult Result = null; + string ProcessLogFile = null; lock (Globals.MainLock) { @@ -217,9 +270,49 @@ namespace Gauntlet Environment.CurrentDirectory = NewWorkingDir; Log.Info("Launching {0} on {1}", App.Name, ToString()); - Log.Verbose("\t{0}", WinApp.CommandArguments); - Result = CommandUtils.Run(WinApp.ExecutablePath, WinApp.CommandArguments, Options: WinApp.RunOptions); + string CmdLine = WinApp.CommandArguments; + + // Look in app parameters if abslog is specified, if so use it + Regex CLRegex = new Regex(@"(--?[a-zA-Z]+)[:\s=]?([A-Z]:(?:\\[\w\s-]+)+\\?(?=\s-)|\""[^\""]*\""|[^-][^\s]*)?"); + foreach (Match M in CLRegex.Matches(CmdLine)) + { + if (M.Groups.Count == 3 && M.Groups[1].Value == "-abslog") + { + ProcessLogFile = M.Groups[2].Value; + } + } + + // explicitly set log file when not already defined + if (string.IsNullOrEmpty(ProcessLogFile)) + { + string LogFolder = string.Format(@"{0}\Logs", WinApp.ArtifactPath); + + if (!Directory.Exists(LogFolder)) + { + Directory.CreateDirectory(LogFolder); + } + + ProcessLogFile = string.Format("{0}\\{1}.log", LogFolder, WinApp.ProjectName); + CmdLine = string.Format("{0} -abslog=\"{1}\"", CmdLine, ProcessLogFile); + } + + // cleanup any existing log file + try + { + if (File.Exists(ProcessLogFile)) + { + File.Delete(ProcessLogFile); + } + } + catch (Exception Ex) + { + throw new AutomationException("Unable to delete existing log file {0} {1}", ProcessLogFile, Ex.Message); + } + + Log.Verbose("\t{0}", CmdLine); + + Result = CommandUtils.Run(WinApp.ExecutablePath, CmdLine, Options: WinApp.RunOptions | (ProcessLogFile != null ? CommandUtils.ERunOptions.NoStdOutRedirect : 0 )); if (Result.HasExited && Result.ExitCode != 0) { @@ -229,7 +322,7 @@ namespace Gauntlet Environment.CurrentDirectory = OldWD; } - return new WindowsAppInstance(WinApp, Result); + return new WindowsAppInstance(WinApp, Result, ProcessLogFile); } protected IAppInstall InstallStagedBuild(UnrealAppConfig AppConfig, StagedBuild InBuild) @@ -254,14 +347,12 @@ namespace Gauntlet Log.Info("Skipping install of {0} (-skipdeploy)", BuildPath); } - // write a token, used to detect and old gauntlet-installedbuilds periodically - string TokenPath = Path.Combine(DestPath, "gauntlet.token"); - File.WriteAllText(TokenPath, "Created by Gauntlet"); + Utils.SystemHelpers.MarkDirectoryForCleanup(DestPath); BuildPath = DestPath; } - WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, this); + WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this); WinApp.RunOptions = RunOptions; // Set commandline replace any InstallPath arguments with the path we use @@ -271,6 +362,8 @@ namespace Gauntlet { WinApp.CommandArguments += string.Format(" -userdir={0}", UserDir); WinApp.ArtifactPath = Path.Combine(UserDir, @"Saved"); + + Utils.SystemHelpers.MarkDirectoryForCleanup(UserDir); } else { @@ -376,7 +469,7 @@ namespace Gauntlet throw new AutomationException("Invalid build type!"); } - WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, this); + WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this); WinApp.WorkingDirectory = Path.GetDirectoryName(EditorBuild.ExecutablePath); WinApp.RunOptions = RunOptions; diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealPGONode.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealPGONode.cs new file mode 100644 index 000000000000..58413f9e7fe5 --- /dev/null +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealPGONode.cs @@ -0,0 +1,190 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +using System; +using System.IO; +using System.Collections.Generic; +using AutomationTool; +using UnrealBuildTool; + +namespace Gauntlet +{ + public class PGOConfig : UnrealTestConfiguration, IAutoParamNotifiable + { + /// + /// Output directory to write the resulting profile data to. + /// + [AutoParam("")] + public string ProfileOutputDirectory; + + /// + /// Directory to save periodic screenshots to whilst the PGO run is in progress. + /// + [AutoParam("")] + public string ScreenshotDirectory; + + [AutoParam("")] + public string PGOAccountSandbox; + + [AutoParam("")] + public string PgcFilenamePrefix; + + public virtual void ParametersWereApplied(string[] Params) + { + if (String.IsNullOrEmpty(ProfileOutputDirectory)) + { + throw new AutomationException("ProfileOutputDirectory option must be specified for profiling data"); + } + } + } + + public class PGONode : UnrealTestNode where TConfigClass : PGOConfig, new() + { + protected string LocalOutputDirectory; + private IPGOPlatform PGOPlatform; + + public PGONode(UnrealTestContext InContext) : base(InContext) + { + + } + + public override TConfigClass GetConfiguration() + { + if (CachedConfig != null) + { + return CachedConfig as TConfigClass; + } + + var Config = CachedConfig = base.GetConfiguration(); + + // Set max duration to 1 hour + Config.MaxDuration = 60 * 60; + + // Get output filenames + LocalOutputDirectory = Path.GetFullPath(Config.ProfileOutputDirectory); + + // Create the local profiling data directory if needed + if (!Directory.Exists(LocalOutputDirectory)) + { + Directory.CreateDirectory(LocalOutputDirectory); + } + + ScreenshotDirectory = Config.ScreenshotDirectory; + + if (!String.IsNullOrEmpty(ScreenshotDirectory)) + { + if (Directory.Exists(ScreenshotDirectory)) + { + Directory.Delete(ScreenshotDirectory, true); + } + + Directory.CreateDirectory(ScreenshotDirectory); + } + + PGOPlatform = PGOPlatformManager.GetPGOPlatform(Context.GetRoleContext(UnrealTargetRole.Client).Platform); + PGOPlatform.ApplyConfiguration(Config); + + return Config as TConfigClass; + + } + + private DateTime ScreenshotStartTime = DateTime.UtcNow; + private DateTime ScreenshotTime = DateTime.MinValue; + private TimeSpan ScreenshotInterval = TimeSpan.FromSeconds(30); + private const float ScreenshotScale = 1.0f / 3.0f; + private const int ScreenshotQuality = 30; + protected string ScreenshotDirectory; + + public override void TickTest() + { + base.TickTest(); + + // Handle device screenshot update + TimeSpan Delta = DateTime.Now - ScreenshotTime; + ITargetDevice Device = TestInstance.ClientApps[0].Device; + + string ImageFilename; + if (!String.IsNullOrEmpty(ScreenshotDirectory) && Delta >= ScreenshotInterval && Device != null && PGOPlatform.TakeScreenshot(Device, ScreenshotDirectory, out ImageFilename)) + { + ScreenshotTime = DateTime.Now; + + try + { + TimeSpan ImageTimestamp = DateTime.UtcNow - ScreenshotStartTime; + string ImageOutputPath = Path.Combine(ScreenshotDirectory, ImageTimestamp.ToString().Replace(':', '-') + ".jpg"); + ImageUtils.ResaveImageAsJpgWithScaleAndQuality(Path.Combine(ScreenshotDirectory, ImageFilename), ImageOutputPath, ScreenshotScale, ScreenshotQuality); + } + catch + { + // Just ignore errors. + } + finally + { + // Delete the temporary image file + try { File.Delete(Path.Combine(ScreenshotDirectory, ImageFilename)); } + catch { } + } + } + + } + + public override void CreateReport(TestResult Result, UnrealTestContext Context, UnrealBuildSource Build, IEnumerable Artifacts, string ArtifactPath) + { + if (Result != TestResult.Passed) + { + return; + } + + // Gather results and merge PGO data + Log.Info("Gathering profiling results..."); + PGOPlatform.GatherResults(TestInstance.ClientApps[0].ArtifactPath); + } + } + + internal interface IPGOPlatform + { + void ApplyConfiguration(PGOConfig Config); + + void GatherResults(string ArtifactPath); + + bool TakeScreenshot(ITargetDevice Device, string ScreenshotDirectory, out string ImageFilename); + + UnrealTargetPlatform GetPlatform(); + } + + /// + /// PGO platform manager + /// + internal abstract class PGOPlatformManager + { + public static IPGOPlatform GetPGOPlatform(UnrealTargetPlatform Platform) + { + Type PGOPlatformType; + if (!PGOPlatforms.TryGetValue(Platform, out PGOPlatformType)) + { + throw new AutomationException("Invalid PGO Platform: {0}", Platform); + } + + return Activator.CreateInstance(PGOPlatformType) as IPGOPlatform; + } + + protected static void RegisterPGOPlatform(UnrealTargetPlatform Platform, Type PGOPlatformType) + { + PGOPlatforms[Platform] = PGOPlatformType; + } + + static Dictionary PGOPlatforms = new Dictionary(); + + static PGOPlatformManager() + { + IEnumerable DiscoveredPGOPlatforms = Utils.InterfaceHelpers.FindImplementations(); + + foreach (IPGOPlatform PGOPlatform in DiscoveredPGOPlatforms) + { + PGOPlatforms[PGOPlatform.GetPlatform()] = PGOPlatform.GetType(); + } + + } + + } + +} diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealSession.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealSession.cs index e4e22aadb34d..48a69071b2d2 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealSession.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealSession.cs @@ -64,6 +64,11 @@ namespace Gauntlet /// public List FilesToCopy; + /// + /// Role device configuration + /// + public ConfigureDeviceHandler ConfigureDevice; + /// /// Properties we require our build to have /// @@ -128,7 +133,7 @@ namespace Gauntlet RequiredBuildFlags |= BuildFlags.CanReplaceExecutable; } - if (Globals.Params.ParseParam("bulk") && InPlatform == UnrealTargetPlatform.Android) + if (Globals.Params.ParseParam("bulk") && (InPlatform == UnrealTargetPlatform.Android || InPlatform == UnrealTargetPlatform.IOS)) { RequiredBuildFlags |= BuildFlags.Bulk; } @@ -794,13 +799,15 @@ namespace Gauntlet InstallSuccess = false; break; } - if (Globals.CancelSignalled) { break; } + // Device has app installed, give role a chance to configure device + Role.ConfigureDevice?.Invoke(Device); + InstallsToRoles[Install] = Role; } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestConfiguration.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestConfiguration.cs index c0a47294b93f..565af2f3e8e8 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestConfiguration.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestConfiguration.cs @@ -43,14 +43,19 @@ namespace Gauntlet Saved } - /// - /// This class represents a process-role in a test and defines the type, command line, - /// and controllers that are needed. - /// - /// TODO - can this be removed and UnrealSessionRole used directly? - /// - /// - public class UnrealTestRole + /// + /// Delegate for role device configuration + /// + public delegate void ConfigureDeviceHandler(ITargetDevice Device); + + /// + /// This class represents a process-role in a test and defines the type, command line, + /// and controllers that are needed. + /// + /// TODO - can this be removed and UnrealSessionRole used directly? + /// + /// + public class UnrealTestRole { /// /// Constructor. This intentionally takes only a type as it's expected that code creating roles should do so via @@ -97,6 +102,12 @@ namespace Gauntlet public string ExplicitClientCommandLine { get; set; } public List FilesToCopy { get; set; } + + /// + /// Role device configuration + /// + public ConfigureDeviceHandler ConfigureDevice; + } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestContext.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestContext.cs index 5f00aa42ef43..0c07d84624ec 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestContext.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestContext.cs @@ -22,6 +22,12 @@ namespace Gauntlet public Params TestParams { get; set; } + public override string ToString() + { + return string.Format("{0}({1})", TestName, string.Join(",", Platforms.Aggregate(new List(), (L, P) => { L.Add(P.Argument); return L; }))); + } + + static public TestRequest CreateRequest(string InName) { return new TestRequest() { TestName = InName, TestParams = new Params(new string[0]) }; } } diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestNode.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestNode.cs index 87d76b05d6ee..4af87d45f98a 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestNode.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/Base/Gauntlet.UnrealTestNode.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using System.Drawing; using System.Linq; using System.Text; +using System.Security.Cryptography; namespace Gauntlet { @@ -83,7 +84,7 @@ namespace Gauntlet /// private TestResult UnrealTestResult; - private TConfigClass CachedConfig = null; + protected TConfigClass CachedConfig = null; private string CachedArtifactPath = null; @@ -350,7 +351,9 @@ namespace Gauntlet SessionRole.RoleModifier = ERoleModifier.Null; } + // copy over relevant settings from test role SessionRole.FilesToCopy = TestRole.FilesToCopy; + SessionRole.ConfigureDevice = TestRole.ConfigureDevice; SessionRoles.Add(SessionRole); } @@ -665,6 +668,55 @@ namespace Gauntlet return ExitCode; } + /// + /// Returns a hash that represents the results of a role. Should be 0 if no fatal errors or ensures + /// + /// + /// + protected virtual string GetRoleResultHash(UnrealRoleArtifacts InArtifacts) + { + const int MaxCallstackLines = 10; + + UnrealLogParser.LogSummary LogSummary = InArtifacts.LogSummary; + + string TotalString = ""; + + //Func ComputeHash = (string Str) => { return Hasher.ComputeHash(Encoding.UTF8.GetBytes(Str)); }; + + if (LogSummary.FatalError != null) + { + TotalString += string.Join("\n", InArtifacts.LogSummary.FatalError.Callstack.Take(MaxCallstackLines)); + TotalString += "\n"; + } + + foreach (var Ensure in LogSummary.Ensures) + { + TotalString += string.Join("\n", Ensure.Callstack.Take(MaxCallstackLines)); + TotalString += "\n"; + } + + string Hash = Hasher.ComputeHash(TotalString); + + return Hash; + } + + /// + /// Returns a hash that represents the failure results of this test. If the test failed this should be an empty string + /// + /// + protected virtual string GetTestResultHash() + { + IEnumerable RoleHashes = SessionArtifacts.Select(A => GetRoleResultHash(A)).OrderBy(S => S); + + RoleHashes = RoleHashes.Where(S => S.Length > 0 && S != "0"); + + string Combined = string.Join("\n", RoleHashes); + + string CombinedHash = Hasher.ComputeHash(Combined); + + return CombinedHash; + } + /// /// Parses the output of an application to try and determine a failure cause (if one exists). Returns /// 0 for graceful shutdown @@ -726,6 +778,8 @@ namespace Gauntlet MB.Paragraph(string.Format("FatalErrors: {0}, Ensures: {1}, Errors: {2}, Warnings: {3}", FatalErrors, LogSummary.Ensures.Count(), LogSummary.Errors.Count(), LogSummary.Warnings.Count())); + MB.Paragraph(string.Format("ResultHash: {0}", GetRoleResultHash(InArtifacts))); + if (GetConfiguration().ShowErrorsInSummary && InArtifacts.LogSummary.Errors.Count() > 0) { MB.H4("Errors"); @@ -944,6 +998,7 @@ namespace Gauntlet } MB.Paragraph(string.Format("Context: {0}", Context.ToString())); MB.Paragraph(string.Format("FatalErrors: {0}, Ensures: {1}, Errors: {2}, Warnings: {3}", FatalErrors, Ensures, Errors, Warnings)); + MB.Paragraph(string.Format("ResultHash: {0}", GetTestResultHash())); //MB.Paragraph(string.Format("Artifacts: {0}", CachedArtifactPath)); MB.Append("--------"); MB.Append(SB.ToString()); diff --git a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/BuildSource/Gauntlet.UnrealBuildSource.cs b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/BuildSource/Gauntlet.UnrealBuildSource.cs index 57fe7c98ab66..55485f4d8b52 100644 --- a/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/BuildSource/Gauntlet.UnrealBuildSource.cs +++ b/Engine/Source/Programs/AutomationTool/Gauntlet/Unreal/BuildSource/Gauntlet.UnrealBuildSource.cs @@ -454,10 +454,80 @@ namespace Gauntlet { Config.FilesToCopy = Role.FilesToCopy; } - + Config.CommandLine = GenerateProcessedCommandLine(Config.CommandLine); return Config; } + /// + /// Remove all duplicate flags and combine any execcmd strings that might be floating around in the commandline. + /// + /// + /// + private string GenerateProcessedCommandLine(string InCommandLine) + { + + // Break down Commandline into individual tokens + Dictionary CommandlineTokens = new Dictionary(StringComparer.OrdinalIgnoreCase); + // turn Name(p1,etc) into a collection of Name|(p1,etc) groups + MatchCollection Matches = Regex.Matches(InCommandLine, "(?Ogg + * encapsulation specification and is based on the self-delimited Opus + * framing described in Appendix B of RFC 6716. + * Normal Opus packets are just a degenerate case of multistream Opus packets, + * and can be encoded or decoded with the multistream API by setting + * streams to 1 when initializing the encoder or + * decoder. + * + * Multistream Opus streams can contain up to 255 elementary Opus streams. + * These may be either "uncoupled" or "coupled", indicating that the decoder + * is configured to decode them to either 1 or 2 channels, respectively. + * The streams are ordered so that all coupled streams appear at the + * beginning. + * + * A mapping table defines which decoded channel i + * should be used for each input/output (I/O) channel j. This table is + * typically provided as an unsigned char array. + * Let i = mapping[j] be the index for I/O channel j. + * If i < 2*coupled_streams, then I/O channel j is + * encoded as the left channel of stream (i/2) if i + * is even, or as the right channel of stream (i/2) if + * i is odd. Otherwise, I/O channel j is encoded as + * mono in stream (i - coupled_streams), unless it has the special + * value 255, in which case it is omitted from the encoding entirely (the + * decoder will reproduce it as silence). Each value i must either + * be the special value 255 or be less than streams + coupled_streams. + * + * The output channels specified by the encoder + * should use the + * Vorbis + * channel ordering. A decoder may wish to apply an additional permutation + * to the mapping the encoder used to achieve a different output channel + * order (e.g. for outputing in WAV order). + * + * Each multistream packet contains an Opus packet for each stream, and all of + * the Opus packets in a single multistream packet must have the same + * duration. Therefore the duration of a multistream packet can be extracted + * from the TOC sequence of the first stream, which is located at the + * beginning of the packet, just like an elementary Opus stream: + * + * @code + * int nb_samples; + * int nb_frames; + * nb_frames = opus_packet_get_nb_frames(data, len); + * if (nb_frames < 1) + * return nb_frames; + * nb_samples = opus_packet_get_samples_per_frame(data, 48000) * nb_frames; + * @endcode + * + * The general encoding and decoding process proceeds exactly the same as in + * the normal @ref opus_encoder and @ref opus_decoder APIs. + * See their documentation for an overview of how to use the corresponding + * multistream functions. + */ + +/** Opus multistream encoder state. + * This contains the complete state of a multistream Opus encoder. + * It is position independent and can be freely copied. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_init + */ +typedef struct OpusMSEncoder OpusMSEncoder; + +/** Opus multistream decoder state. + * This contains the complete state of a multistream Opus decoder. + * It is position independent and can be freely copied. + * @see opus_multistream_decoder_create + * @see opus_multistream_decoder_init + */ +typedef struct OpusMSDecoder OpusMSDecoder; + +/**\name Multistream encoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSEncoder structure. + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_encoder_get_size( + int streams, + int coupled_streams +); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_surround_encoder_get_size( + int channels, + int mapping_family +); + + +/** Allocates and initializes a multistream encoder state. + * Call opus_multistream_encoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_encoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSEncoder *opus_multistream_surround_encoder_create( + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application, + int *error +) OPUS_ARG_NONNULL(5); + +/** Initialize a previously allocated multistream encoder state. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_encoder_create + * @see opus_multistream_encoder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate of the input signal (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels in the input signal. + * This must be at most 255. + * It may be greater than the number of + * coded channels (streams + + * coupled_streams). + * @param streams int: The total number of streams to encode from the + * input. + * This must be no more than the number of channels. + * @param coupled_streams int: Number of coupled (2 channel) streams + * to encode. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * encoded channels (streams + + * coupled_streams) must be no + * more than the number of input channels. + * @param[in] mapping const unsigned char[channels]: Mapping from + * encoded channels to input channels, as described in + * @ref opus_multistream. As an extra constraint, the + * multistream encoder does not allow encoding coupled + * streams for which one channel is unused since this + * is never a good idea. + * @param application int: The target encoder application. + * This must be one of the following: + *
+ *
#OPUS_APPLICATION_VOIP
+ *
Process signal for improved speech intelligibility.
+ *
#OPUS_APPLICATION_AUDIO
+ *
Favor faithfulness to the original input.
+ *
#OPUS_APPLICATION_RESTRICTED_LOWDELAY
+ *
Configure the minimum possible coding delay by disabling certain modes + * of operation.
+ *
+ * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +OPUS_EXPORT int opus_multistream_surround_encoder_init( + OpusMSEncoder *st, + opus_int32 Fs, + int channels, + int mapping_family, + int *streams, + int *coupled_streams, + unsigned char *mapping, + int application +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Encodes a multistream Opus frame. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const opus_int16*: The input signal as interleaved + * samples. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode( + OpusMSEncoder *st, + const opus_int16 *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Encodes a multistream Opus frame from floating point input. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param[in] pcm const float*: The input signal as interleaved + * samples with a normal range of + * +/-1.0. + * Samples with a range beyond +/-1.0 + * are supported but will be clipped by + * decoders using the integer API and + * should only be used if it is known + * that the far end supports extended + * dynamic range. + * This must contain + * frame_size*channels + * samples. + * @param frame_size int: Number of samples per channel in the input + * signal. + * This must be an Opus frame size for the + * encoder's sampling rate. + * For example, at 48 kHz the permitted values + * are 120, 240, 480, 960, 1920, and 2880. + * Passing in a duration of less than 10 ms + * (480 samples at 48 kHz) will prevent the + * encoder from using the LPC or hybrid modes. + * @param[out] data unsigned char*: Output payload. + * This must contain storage for at + * least \a max_data_bytes. + * @param [in] max_data_bytes opus_int32: Size of the allocated + * memory for the output + * payload. This may be + * used to impose an upper limit on + * the instant bitrate, but should + * not be used as the only bitrate + * control. Use #OPUS_SET_BITRATE to + * control the bitrate. + * @returns The length of the encoded packet (in bytes) on success or a + * negative error code (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_encode_float( + OpusMSEncoder *st, + const float *pcm, + int frame_size, + unsigned char *data, + opus_int32 max_data_bytes +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(2) OPUS_ARG_NONNULL(4); + +/** Frees an OpusMSEncoder allocated by + * opus_multistream_encoder_create(). + * @param st OpusMSEncoder*: Multistream encoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_encoder_destroy(OpusMSEncoder *st); + +/** Perform a CTL function on a multistream Opus encoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSEncoder*: Multistream encoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_encoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_encoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_encoder_ctl(OpusMSEncoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/**@}*/ + +/**\name Multistream decoder functions */ +/**@{*/ + +/** Gets the size of an OpusMSDecoder structure. + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @returns The size in bytes on success, or a negative error code + * (see @ref opus_errorcodes) on error. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_multistream_decoder_get_size( + int streams, + int coupled_streams +); + +/** Allocates and initializes a multistream decoder state. + * Call opus_multistream_decoder_destroy() to release + * this object when finished. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @param[out] error int *: Returns #OPUS_OK on success, or an error + * code (see @ref opus_errorcodes) on + * failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusMSDecoder *opus_multistream_decoder_create( + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping, + int *error +) OPUS_ARG_NONNULL(5); + +/** Intialize a previously allocated decoder state object. + * The memory pointed to by \a st must be at least the size returned by + * opus_multistream_encoder_get_size(). + * This is intended for applications which use their own allocator instead of + * malloc. + * To reset a previously initialized state, use the #OPUS_RESET_STATE CTL. + * @see opus_multistream_decoder_create + * @see opus_multistream_deocder_get_size + * @param st OpusMSEncoder*: Multistream encoder state to initialize. + * @param Fs opus_int32: Sampling rate to decode at (in Hz). + * This must be one of 8000, 12000, 16000, + * 24000, or 48000. + * @param channels int: Number of channels to output. + * This must be at most 255. + * It may be different from the number of coded + * channels (streams + + * coupled_streams). + * @param streams int: The total number of streams coded in the + * input. + * This must be no more than 255. + * @param coupled_streams int: Number of streams to decode as coupled + * (2 channel) streams. + * This must be no larger than the total + * number of streams. + * Additionally, The total number of + * coded channels (streams + + * coupled_streams) must be no + * more than 255. + * @param[in] mapping const unsigned char[channels]: Mapping from + * coded channels to output channels, as described in + * @ref opus_multistream. + * @returns #OPUS_OK on success, or an error code (see @ref opus_errorcodes) + * on failure. + */ +OPUS_EXPORT int opus_multistream_decoder_init( + OpusMSDecoder *st, + opus_int32 Fs, + int channels, + int streams, + int coupled_streams, + const unsigned char *mapping +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(6); + +/** Decode a multistream Opus packet. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + opus_int16 *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Decode a multistream Opus packet with floating point output. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param[in] data const unsigned char*: Input payload. + * Use a NULL + * pointer to indicate packet + * loss. + * @param len opus_int32: Number of bytes in payload. + * @param[out] pcm opus_int16*: Output signal, with interleaved + * samples. + * This must contain room for + * frame_size*channels + * samples. + * @param frame_size int: The number of samples per channel of + * available space in \a pcm. + * If this is less than the maximum packet duration + * (120 ms; 5760 for 48kHz), this function will not be capable + * of decoding some packets. In the case of PLC (data==NULL) + * or FEC (decode_fec=1), then frame_size needs to be exactly + * the duration of audio that is missing, otherwise the + * decoder will not be in the optimal state to decode the + * next incoming packet. For the PLC and FEC cases, frame_size + * must be a multiple of 2.5 ms. + * @param decode_fec int: Flag (0 or 1) to request that any in-band + * forward error correction data be decoded. + * If no such data is available, the frame is + * decoded as if it were lost. + * @returns Number of samples decoded on success or a negative error code + * (see @ref opus_errorcodes) on failure. + */ +OPUS_EXPORT OPUS_WARN_UNUSED_RESULT int opus_multistream_decode_float( + OpusMSDecoder *st, + const unsigned char *data, + opus_int32 len, + float *pcm, + int frame_size, + int decode_fec +) OPUS_ARG_NONNULL(1) OPUS_ARG_NONNULL(4); + +/** Perform a CTL function on a multistream Opus decoder. + * + * Generally the request and subsequent arguments are generated by a + * convenience macro. + * @param st OpusMSDecoder*: Multistream decoder state. + * @param request This and all remaining parameters should be replaced by one + * of the convenience macros in @ref opus_genericctls, + * @ref opus_decoderctls, or @ref opus_multistream_ctls. + * @see opus_genericctls + * @see opus_decoderctls + * @see opus_multistream_ctls + */ +OPUS_EXPORT int opus_multistream_decoder_ctl(OpusMSDecoder *st, int request, ...) OPUS_ARG_NONNULL(1); + +/** Frees an OpusMSDecoder allocated by + * opus_multistream_decoder_create(). + * @param st OpusMSDecoder: Multistream decoder state to be freed. + */ +OPUS_EXPORT void opus_multistream_decoder_destroy(OpusMSDecoder *st); + +/**@}*/ + +/**@}*/ + +#ifdef __cplusplus +} +#endif + +#endif /* OPUS_MULTISTREAM_H */ diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/opus_types.h b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/opus_types.h new file mode 100644 index 000000000000..71808266655a --- /dev/null +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/opus_types.h @@ -0,0 +1,159 @@ +/* (C) COPYRIGHT 1994-2002 Xiph.Org Foundation */ +/* Modified by Jean-Marc Valin */ +/* + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +/* opus_types.h based on ogg_types.h from libogg */ + +/** + @file opus_types.h + @brief Opus reference implementation types +*/ +#ifndef OPUS_TYPES_H +#define OPUS_TYPES_H + +/* Use the real stdint.h if it's there (taken from Paul Hsieh's pstdint.h) */ +#if (defined(__STDC__) && __STDC__ && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_)) || defined (HAVE_STDINT_H)) +#include + + typedef int16_t opus_int16; + typedef uint16_t opus_uint16; + typedef int32_t opus_int32; + typedef uint32_t opus_uint32; +#elif defined(_WIN32) + +# if defined(__CYGWIN__) +# include <_G_config.h> + typedef _G_int32_t opus_int32; + typedef _G_uint32_t opus_uint32; + typedef _G_int16 opus_int16; + typedef _G_uint16 opus_uint16; +# elif defined(__MINGW32__) + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; +# elif defined(__MWERKS__) + typedef int opus_int32; + typedef unsigned int opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; +# else + /* MSVC/Borland */ + typedef __int32 opus_int32; + typedef unsigned __int32 opus_uint32; + typedef __int16 opus_int16; + typedef unsigned __int16 opus_uint16; +# endif + +#elif defined(__MACOS__) + +# include + typedef SInt16 opus_int16; + typedef UInt16 opus_uint16; + typedef SInt32 opus_int32; + typedef UInt32 opus_uint32; + +#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ + +# include + typedef int16_t opus_int16; + typedef u_int16_t opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined(__BEOS__) + + /* Be */ +# include + typedef int16 opus_int16; + typedef u_int16 opus_uint16; + typedef int32_t opus_int32; + typedef u_int32_t opus_uint32; + +#elif defined (__EMX__) + + /* OS/2 GCC */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined (DJGPP) + + /* DJGPP */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(R5900) + + /* PS2 EE */ + typedef int opus_int32; + typedef unsigned opus_uint32; + typedef short opus_int16; + typedef unsigned short opus_uint16; + +#elif defined(__SYMBIAN32__) + + /* Symbian GCC */ + typedef signed short opus_int16; + typedef unsigned short opus_uint16; + typedef signed int opus_int32; + typedef unsigned int opus_uint32; + +#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef long opus_int32; + typedef unsigned long opus_uint32; + +#elif defined(CONFIG_TI_C6X) + + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#else + + /* Give up, take a reasonable guess */ + typedef short opus_int16; + typedef unsigned short opus_uint16; + typedef int opus_int32; + typedef unsigned int opus_uint32; + +#endif + +#define opus_int int /* used for counters etc; at least 16 bits */ +#define opus_int64 long long +#define opus_int8 signed char + +#define opus_uint unsigned int /* used for counters etc; at least 16 bits */ +#define opus_uint64 unsigned long long +#define opus_uint8 unsigned char + +#endif /* OPUS_TYPES_H */ diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/speex_resampler.h b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/speex_resampler.h new file mode 100644 index 000000000000..6bd479a338b5 --- /dev/null +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/include/speex_resampler.h @@ -0,0 +1,350 @@ +/* Copyright (C) 2007 Jean-Marc Valin + + File: speex_resampler.h + Resampling code + + The design goals of this code are: + - Very fast algorithm + - Low memory requirement + - Good *perceptual* quality (and not best SNR) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + + +#ifndef SPEEX_RESAMPLER_H +#define SPEEX_RESAMPLER_H + +#ifdef ANDROID +#ifndef OUTSIDE_SPEEX +#define OUTSIDE_SPEEX +#endif +#endif + +#ifdef OUTSIDE_SPEEX + +/********* WARNING: MENTAL SANITY ENDS HERE *************/ + +/* If the resampler is defined outside of Speex, we change the symbol names so that + there won't be any clash if linking with Speex later on. */ + +#define RANDOM_PREFIX Opus +#ifndef RANDOM_PREFIX +#error "Please define RANDOM_PREFIX (above) to something specific to your project to prevent symbol name clashes" +#endif + +#define CAT_PREFIX2(a,b) a ## b +#define CAT_PREFIX(a,b) CAT_PREFIX2(a, b) + +#define speex_resampler_init CAT_PREFIX(RANDOM_PREFIX,_resampler_init) +#define speex_resampler_init_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_init_frac) +#define speex_resampler_destroy CAT_PREFIX(RANDOM_PREFIX,_resampler_destroy) +#define speex_resampler_process_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_float) +#define speex_resampler_process_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_int) +#define speex_resampler_process_interleaved_float CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_float) +#define speex_resampler_process_interleaved_int CAT_PREFIX(RANDOM_PREFIX,_resampler_process_interleaved_int) +#define speex_resampler_set_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate) +#define speex_resampler_get_rate CAT_PREFIX(RANDOM_PREFIX,_resampler_get_rate) +#define speex_resampler_set_rate_frac CAT_PREFIX(RANDOM_PREFIX,_resampler_set_rate_frac) +#define speex_resampler_get_ratio CAT_PREFIX(RANDOM_PREFIX,_resampler_get_ratio) +#define speex_resampler_set_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_set_quality) +#define speex_resampler_get_quality CAT_PREFIX(RANDOM_PREFIX,_resampler_get_quality) +#define speex_resampler_set_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_input_stride) +#define speex_resampler_get_input_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_stride) +#define speex_resampler_set_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_set_output_stride) +#define speex_resampler_get_output_stride CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_stride) +#define speex_resampler_get_input_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_input_latency) +#define speex_resampler_get_output_latency CAT_PREFIX(RANDOM_PREFIX,_resampler_get_output_latency) +#define speex_resampler_skip_zeros CAT_PREFIX(RANDOM_PREFIX,_resampler_skip_zeros) +#define speex_resampler_reset_mem CAT_PREFIX(RANDOM_PREFIX,_resampler_reset_mem) +#define speex_resampler_strerror CAT_PREFIX(RANDOM_PREFIX,_resampler_strerror) + +#define spx_int16_t short +#define spx_int32_t int +#define spx_uint16_t unsigned short +#define spx_uint32_t unsigned int + +#else /* OUTSIDE_SPEEX */ + +#ifdef _BUILD_SPEEX +# include "speex_types.h" +#else +# include +#endif + +#endif /* OUTSIDE_SPEEX */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define SPEEX_RESAMPLER_QUALITY_MAX 10 +#define SPEEX_RESAMPLER_QUALITY_MIN 0 +#define SPEEX_RESAMPLER_QUALITY_DEFAULT 4 +#define SPEEX_RESAMPLER_QUALITY_VOIP 3 +#define SPEEX_RESAMPLER_QUALITY_DESKTOP 5 + +enum { + RESAMPLER_ERR_SUCCESS = 0, + RESAMPLER_ERR_ALLOC_FAILED = 1, + RESAMPLER_ERR_BAD_STATE = 2, + RESAMPLER_ERR_INVALID_ARG = 3, + RESAMPLER_ERR_PTR_OVERLAP = 4, + + RESAMPLER_ERR_MAX_ERROR +}; + +struct SpeexResamplerState_; +typedef struct SpeexResamplerState_ SpeexResamplerState; + +/** Create a new resampler with integer input and output rates. + * @param nb_channels Number of channels to be processed + * @param in_rate Input sampling rate (integer number of Hz). + * @param out_rate Output sampling rate (integer number of Hz). + * @param quality Resampling quality between 0 and 10, where 0 has poor quality + * and 10 has very high quality. + * @return Newly created resampler state + * @retval NULL Error: not enough memory + */ +SpeexResamplerState *speex_resampler_init(spx_uint32_t nb_channels, + spx_uint32_t in_rate, + spx_uint32_t out_rate, + int quality, + int *err); + +/** Create a new resampler with fractional input/output rates. The sampling + * rate ratio is an arbitrary rational number with both the numerator and + * denominator being 32-bit integers. + * @param nb_channels Number of channels to be processed + * @param ratio_num Numerator of the sampling rate ratio + * @param ratio_den Denominator of the sampling rate ratio + * @param in_rate Input sampling rate rounded to the nearest integer (in Hz). + * @param out_rate Output sampling rate rounded to the nearest integer (in Hz). + * @param quality Resampling quality between 0 and 10, where 0 has poor quality + * and 10 has very high quality. + * @return Newly created resampler state + * @retval NULL Error: not enough memory + */ +SpeexResamplerState *speex_resampler_init_frac(spx_uint32_t nb_channels, + spx_uint32_t ratio_num, + spx_uint32_t ratio_den, + spx_uint32_t in_rate, + spx_uint32_t out_rate, + int quality, + int *err); + +/** Destroy a resampler state. + * @param st Resampler state + */ +void speex_resampler_destroy(SpeexResamplerState *st); + +/** Resample a float array. The input and output buffers must *not* overlap. + * @param st Resampler state + * @param channel_index Index of the channel to process for the multi-channel + * base (0 otherwise) + * @param in Input buffer + * @param in_len Number of input samples in the input buffer. Returns the + * number of samples processed + * @param out Output buffer + * @param out_len Size of the output buffer. Returns the number of samples written + */ +int speex_resampler_process_float(SpeexResamplerState *st, + spx_uint32_t channel_index, + const float *in, + spx_uint32_t *in_len, + float *out, + spx_uint32_t *out_len); + +/** Resample an int array. The input and output buffers must *not* overlap. + * @param st Resampler state + * @param channel_index Index of the channel to process for the multi-channel + * base (0 otherwise) + * @param in Input buffer + * @param in_len Number of input samples in the input buffer. Returns the number + * of samples processed + * @param out Output buffer + * @param out_len Size of the output buffer. Returns the number of samples written + */ +int speex_resampler_process_int(SpeexResamplerState *st, + spx_uint32_t channel_index, + const spx_int16_t *in, + spx_uint32_t *in_len, + spx_int16_t *out, + spx_uint32_t *out_len); + +/** Resample an interleaved float array. The input and output buffers must *not* overlap. + * @param st Resampler state + * @param in Input buffer + * @param in_len Number of input samples in the input buffer. Returns the number + * of samples processed. This is all per-channel. + * @param out Output buffer + * @param out_len Size of the output buffer. Returns the number of samples written. + * This is all per-channel. + */ +int speex_resampler_process_interleaved_float(SpeexResamplerState *st, + const float *in, + spx_uint32_t *in_len, + float *out, + spx_uint32_t *out_len); + +/** Resample an interleaved int array. The input and output buffers must *not* overlap. + * @param st Resampler state + * @param in Input buffer + * @param in_len Number of input samples in the input buffer. Returns the number + * of samples processed. This is all per-channel. + * @param out Output buffer + * @param out_len Size of the output buffer. Returns the number of samples written. + * This is all per-channel. + */ +int speex_resampler_process_interleaved_int(SpeexResamplerState *st, + const spx_int16_t *in, + spx_uint32_t *in_len, + spx_int16_t *out, + spx_uint32_t *out_len); + +/** Set (change) the input/output sampling rates (integer value). + * @param st Resampler state + * @param in_rate Input sampling rate (integer number of Hz). + * @param out_rate Output sampling rate (integer number of Hz). + */ +int speex_resampler_set_rate(SpeexResamplerState *st, + spx_uint32_t in_rate, + spx_uint32_t out_rate); + +/** Get the current input/output sampling rates (integer value). + * @param st Resampler state + * @param in_rate Input sampling rate (integer number of Hz) copied. + * @param out_rate Output sampling rate (integer number of Hz) copied. + */ +void speex_resampler_get_rate(SpeexResamplerState *st, + spx_uint32_t *in_rate, + spx_uint32_t *out_rate); + +/** Set (change) the input/output sampling rates and resampling ratio + * (fractional values in Hz supported). + * @param st Resampler state + * @param ratio_num Numerator of the sampling rate ratio + * @param ratio_den Denominator of the sampling rate ratio + * @param in_rate Input sampling rate rounded to the nearest integer (in Hz). + * @param out_rate Output sampling rate rounded to the nearest integer (in Hz). + */ +int speex_resampler_set_rate_frac(SpeexResamplerState *st, + spx_uint32_t ratio_num, + spx_uint32_t ratio_den, + spx_uint32_t in_rate, + spx_uint32_t out_rate); + +/** Get the current resampling ratio. This will be reduced to the least + * common denominator. + * @param st Resampler state + * @param ratio_num Numerator of the sampling rate ratio copied + * @param ratio_den Denominator of the sampling rate ratio copied + */ +void speex_resampler_get_ratio(SpeexResamplerState *st, + spx_uint32_t *ratio_num, + spx_uint32_t *ratio_den); + +/** Set (change) the conversion quality. + * @param st Resampler state + * @param quality Resampling quality between 0 and 10, where 0 has poor + * quality and 10 has very high quality. + */ +int speex_resampler_set_quality(SpeexResamplerState *st, + int quality); + +/** Get the conversion quality. + * @param st Resampler state + * @param quality Resampling quality between 0 and 10, where 0 has poor + * quality and 10 has very high quality. + */ +void speex_resampler_get_quality(SpeexResamplerState *st, + int *quality); + +/** Set (change) the input stride. + * @param st Resampler state + * @param stride Input stride + */ +void speex_resampler_set_input_stride(SpeexResamplerState *st, + spx_uint32_t stride); + +/** Get the input stride. + * @param st Resampler state + * @param stride Input stride copied + */ +void speex_resampler_get_input_stride(SpeexResamplerState *st, + spx_uint32_t *stride); + +/** Set (change) the output stride. + * @param st Resampler state + * @param stride Output stride + */ +void speex_resampler_set_output_stride(SpeexResamplerState *st, + spx_uint32_t stride); + +/** Get the output stride. + * @param st Resampler state copied + * @param stride Output stride + */ +void speex_resampler_get_output_stride(SpeexResamplerState *st, + spx_uint32_t *stride); + +/** Get the latency introduced by the resampler measured in input samples. + * @param st Resampler state + */ +int speex_resampler_get_input_latency(SpeexResamplerState *st); + +/** Get the latency introduced by the resampler measured in output samples. + * @param st Resampler state + */ +int speex_resampler_get_output_latency(SpeexResamplerState *st); + +/** Make sure that the first samples to go out of the resamplers don't have + * leading zeros. This is only useful before starting to use a newly created + * resampler. It is recommended to use that when resampling an audio file, as + * it will generate a file with the same length. For real-time processing, + * it is probably easier not to use this call (so that the output duration + * is the same for the first frame). + * @param st Resampler state + */ +int speex_resampler_skip_zeros(SpeexResamplerState *st); + +/** Reset a resampler so a new (unrelated) stream can be processed. + * @param st Resampler state + */ +int speex_resampler_reset_mem(SpeexResamplerState *st); + +/** Returns the English meaning for an error code + * @param err Error code + * @return English string + */ +const char *speex_resampler_strerror(int err); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/version.h b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/version.h new file mode 100644 index 000000000000..a282f2f38fbd --- /dev/null +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/IOS/libOpus/version.h @@ -0,0 +1 @@ +#define PACKAGE_VERSION "1.1-beta" diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/celt.vcxproj b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/celt.vcxproj index e10ab7df398a..222edacd63c8 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/celt.vcxproj +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/celt.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -73,33 +73,34 @@ {245603E3-F580-41A5-9632-B25FE3372CBF} Win32Proj celt + 10.0.17134.0 StaticLibrary true Unicode - v110 + v141 StaticLibrary true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus.vcxproj b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus.vcxproj index 7dcda7f81d21..7a75725889d1 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus.vcxproj +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -22,28 +22,29 @@ Win32Proj opus {219EC965-228A-1824-174D-96449D05F88A} + 10.0.17134.0 StaticLibrary true - v110 + v141 StaticLibrary true - v110 + v141 false StaticLibrary true - v110 + v141 StaticLibrary false - v110 + v141 diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus_demo.vcxproj b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus_demo.vcxproj index 2401a24db7eb..58d2fbdbce99 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus_demo.vcxproj +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/opus_demo.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -22,33 +22,34 @@ {016C739D-6389-43BF-8D88-24B2BF6F620F} Win32Proj opus_demo + 10.0.17134.0 Application true Unicode - v110 + v141 Application true Unicode - v110 + v141 Application false true Unicode - v110 + v141 Application false true Unicode - v110 + v141 diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_common.vcxproj b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_common.vcxproj index 4329756b3004..d58e9229b98d 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_common.vcxproj +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_common.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -23,33 +23,34 @@ Win32Proj src_common silk_common + 10.0.17134.0 StaticLibrary true Unicode - v110 + v141 StaticLibrary true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_fixed.vcxproj b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_fixed.vcxproj index fa2e02249617..6941027e02c4 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_fixed.vcxproj +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/Windows/VS2012/silk_fixed.vcxproj @@ -1,5 +1,5 @@  - + Debug @@ -23,33 +23,34 @@ Win32Proj src_FIX silk_fixed + 10.0.17134.0 StaticLibrary true Unicode - v110 + v141 StaticLibrary true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 StaticLibrary false true Unicode - v110 + v141 diff --git a/Engine/Source/ThirdParty/libOpus/opus-1.1/speex_resampler/arch.h b/Engine/Source/ThirdParty/libOpus/opus-1.1/speex_resampler/arch.h index 3d48cfb8881d..c84c1ae2667f 100644 --- a/Engine/Source/ThirdParty/libOpus/opus-1.1/speex_resampler/arch.h +++ b/Engine/Source/ThirdParty/libOpus/opus-1.1/speex_resampler/arch.h @@ -76,7 +76,9 @@ #endif -#ifndef OUTSIDE_SPEEX +#ifdef DEFINED_SPEEX_TYPES +#include "speex_types.h" +#elif !defined(OUTSIDE_SPEEX) #include "../include/speex/speex_types.h" #endif