Compare commits

...

2 Commits

Author SHA1 Message Date
Ivan Anikin 86baa476f4 Refactoring 2026-05-20 15:33:00 +07:00
Ivan Anikin 8732ce99e3 Refactoring 2026-05-19 22:17:35 +07:00
56 changed files with 335 additions and 1539 deletions
@@ -19,7 +19,7 @@ public static final XAPKFile[] xAPKS = {
new XAPKFile( new XAPKFile(
true, // true signifies a main file true, // true signifies a main file
"1", // the version of the APK that the file was uploaded against "1", // the version of the APK that the file was uploaded against
295265920L // the length of the file in bytes 259568643L // the length of the file in bytes
) )
}; };
}; };
+24
View File
@@ -0,0 +1,24 @@
[/Script/Engine.DeviceProfileManager]
[Android_Mali_G77_Vulkan DeviceProfile]
DeviceType=Android
BaseProfileName=Android_Mali_G77
+CVars=r.Android.DisableVulkanSupport=0
+CVars=t.maxfps=35
+CVars=r.AllowOcclusionQueries=0
+CVars=r.OcclusionFeedback.Enable=0
+CVars=r.HZBOcclusion=0
+CVars=r.ScreenPercentage=80
+CVars=sg.ViewDistanceQuality=2
+CVars=sg.AntiAliasingQuality=1
+CVars=sg.ShadowQuality=1
+CVars=sg.PostProcessQuality=1
+CVars=sg.TextureQuality=2
+CVars=sg.EffectsQuality=4
+CVars=sg.FoliageQuality=1
+CVars=sg.ShadingQuality=2
+CVars=r.BloomQuality=2
+CVars=r.Mobile.AmbientOcclusion=0
+CVars=r.Mobile.ScreenSpaceReflections=0
+CVars=r.Streaming.PoolSize=3000
+CVars=r.MaxAnisotropy=4
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -1,5 +1,8 @@
[/Script/Engine.Engine]
+ActiveClassRedirects=(OldClassName="/Script/AndroidRadioPlayer.AndroidRadioPlayerBPLibrary",NewClassName="/Script/FunnyGirl.FunnyGirlUtilityBlueprintLibrary")
[/Script/EngineSettings.GameMapsSettings] [/Script/EngineSettings.GameMapsSettings]
EditorStartupMap=/Game/Game/Maps/M_GirlHouse.M_GirlHouse EditorStartupMap=/Game/Game/Maps/M_GirlHouse.M_GirlHouse
LocalMapOptions= LocalMapOptions=
@@ -265,7 +268,7 @@ UIScaleCurve=(EditorCurveData=(Keys=((Time=480.000000,Value=0.440000),(Time=576.
bExplicitCanvasChildZOrder=True bExplicitCanvasChildZOrder=True
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=False bEnablePlugin=True
bAllowNetworkConnection=True bAllowNetworkConnection=True
SecurityToken=E63F664EDF4A840AACB2E4A0C6D55FC6 SecurityToken=E63F664EDF4A840AACB2E4A0C6D55FC6
bIncludeInShipping=False bIncludeInShipping=False
-2
View File
@@ -109,7 +109,5 @@ bSkipMovies=False
+IniSectionDenylist=StorageServers +IniSectionDenylist=StorageServers
+IniSectionDenylist=/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings +IniSectionDenylist=/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings
+MapsToCook=(FilePath="/Game/Game/Maps/M_GirlHouse") +MapsToCook=(FilePath="/Game/Game/Maps/M_GirlHouse")
+DirectoriesToAlwaysCook=(Path="/NNEDenoiser")
bRetainStagedDirectory=False bRetainStagedDirectory=False
CustomStageCopyHandler= CustomStageCopyHandler=
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,19 +0,0 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Android Radio Player",
"Description": "Blueprint radio playback for Android using AndroidX Media3 ExoPlayer.",
"Category": "Media Players",
"CreatedBy": "FunnyGirl",
"CanContainContent": false,
"IsBetaVersion": true,
"EnabledByDefault": true,
"Modules": [
{
"Name": "AndroidRadioPlayer",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
}
@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
<init>
<log text="AndroidRadioPlayer init"/>
</init>
<androidManifestUpdates>
<addPermission android:name="android.permission.INTERNET"/>
<addAttribute tag="application" name="android:usesCleartextTraffic" value="true"/>
</androidManifestUpdates>
<buildGradleAdditions>
<insert>
dependencies {
implementation('androidx.media3:media3-exoplayer:1.3.1')
implementation('androidx.media3:media3-exoplayer-hls:1.3.1')
}
</insert>
</buildGradleAdditions>
<prebuildCopies>
<log text="Copying AndroidRadioPlayer Java source"/>
<copyFile src="$S(PluginDir)/Java/AndroidRadioPlayer.java" dst="$S(BuildDir)/src/com/epicgames/unreal/AndroidRadioPlayer.java"/>
</prebuildCopies>
<proguardAdditions>
<insert>
-keep class com.epicgames.unreal.AndroidRadioPlayer {
public *;
}
</insert>
</proguardAdditions>
<gameActivityOnPauseAdditions>
<insert>
AndroidRadioPlayer.handleActivityPause();
</insert>
</gameActivityOnPauseAdditions>
<gameActivityOnResumeAdditions>
<insert>
AndroidRadioPlayer.handleActivityResume();
</insert>
</gameActivityOnResumeAdditions>
<gameActivityOnDestroyAdditions>
<insert>
AndroidRadioPlayer.releaseRadio();
</insert>
</gameActivityOnDestroyAdditions>
</root>
@@ -1,274 +0,0 @@
package com.epicgames.unreal;
import android.app.Activity;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
public final class AndroidRadioPlayer {
private static final String TAG = "AndroidRadioPlayer";
private static final int STATE_IDLE = 0;
private static final int STATE_BUFFERING = 1;
private static final int STATE_READY = 2;
private static final int STATE_ENDED = 3;
private static final int STATE_ERROR = 4;
private static ExoPlayer player;
private static volatile int playbackState = STATE_IDLE;
private static volatile String lastError = "";
private static volatile boolean isPlaying = false;
private static volatile boolean shouldResumeAfterActivityPause = false;
private static volatile long cachedDurationMillis = 0;
private static volatile long cachedCurrentPositionMillis = 0;
private static float pendingVolume = 1.0f;
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private AndroidRadioPlayer() {
}
public static boolean playRadio(final Activity activity, final String url, final float volume) {
if (activity == null) {
lastError = "Activity is null";
playbackState = STATE_ERROR;
return false;
}
if (url == null || url.trim().isEmpty()) {
lastError = "Radio URL is empty";
playbackState = STATE_ERROR;
return false;
}
pendingVolume = clampVolume(volume);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
ensurePlayer(activity);
lastError = "";
playbackState = STATE_BUFFERING;
player.stop();
player.clearMediaItems();
isPlaying = false;
player.setVolume(pendingVolume);
player.setMediaItem(buildMediaItem(url.trim()));
player.prepare();
player.play();
} catch (Throwable error) {
lastError = error.toString();
playbackState = STATE_ERROR;
Log.e(TAG, "Failed to play radio", error);
}
}
});
return true;
}
public static void stopRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
releasePlayer();
}
});
}
public static void releaseRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
releasePlayer();
}
});
}
public static void pauseRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.pause();
}
}
});
}
public static void resumeRadio() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.play();
}
}
});
}
public static void setRadioVolume(final float volume) {
pendingVolume = clampVolume(volume);
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null) {
player.setVolume(pendingVolume);
}
}
});
}
public static void handleActivityPause() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
shouldResumeAfterActivityPause = player != null && player.isPlaying();
if (player != null && shouldResumeAfterActivityPause) {
player.pause();
}
}
});
}
public static void handleActivityResume() {
runOnPlayerThread(new Runnable() {
@Override
public void run() {
if (player != null && shouldResumeAfterActivityPause) {
shouldResumeAfterActivityPause = false;
player.play();
}
}
});
}
public static boolean isRadioPlaying() {
return isPlaying;
}
public static int getPlaybackState() {
return playbackState;
}
public static String getLastError() {
return lastError == null ? "" : lastError;
}
public static long getDurationMillis() {
return cachedDurationMillis;
}
public static long getCurrentPositionMillis() {
return cachedCurrentPositionMillis;
}
private static void ensurePlayer(Activity activity) {
if (player != null) {
return;
}
DefaultLoadControl loadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(5000, 15000, 1000, 2000)
.build();
player = new ExoPlayer.Builder(activity.getApplicationContext())
.setLoadControl(loadControl)
.setHandleAudioBecomingNoisy(true)
.build();
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
player.setAudioAttributes(audioAttributes, true);
player.setVolume(pendingVolume);
player.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_BUFFERING) {
playbackState = STATE_BUFFERING;
} else if (state == Player.STATE_READY) {
playbackState = STATE_READY;
long duration = player.getDuration();
cachedDurationMillis = duration == C.TIME_UNSET ? 0 : duration;
} else if (state == Player.STATE_ENDED) {
playbackState = STATE_ENDED;
} else {
playbackState = STATE_IDLE;
}
}
@Override
public void onIsPlayingChanged(boolean playing) {
isPlaying = playing;
}
@Override
public void onPlayerError(PlaybackException error) {
lastError = error == null ? "Unknown ExoPlayer error" : error.toString();
playbackState = STATE_ERROR;
isPlaying = false;
Log.e(TAG, "Playback error", error);
}
});
}
private static void releasePlayer() {
if (player != null) {
try {
player.stop();
player.clearMediaItems();
player.release();
} catch (Throwable error) {
Log.w(TAG, "Failed to release radio player cleanly", error);
}
player = null;
}
shouldResumeAfterActivityPause = false;
isPlaying = false;
cachedDurationMillis = 0;
cachedCurrentPositionMillis = 0;
playbackState = STATE_IDLE;
}
private static MediaItem buildMediaItem(String url) {
MediaItem.Builder builder = new MediaItem.Builder().setUri(Uri.parse(url));
String lowerUrl = url.toLowerCase();
if (lowerUrl.contains(".m3u8") || lowerUrl.contains("m3u8")) {
builder.setMimeType(MimeTypes.APPLICATION_M3U8);
} else if (lowerUrl.contains("aacp") || lowerUrl.contains(".aac") || lowerUrl.contains("audio/aac")) {
builder.setMimeType(MimeTypes.AUDIO_AAC);
} else if (lowerUrl.contains(".mp3") || lowerUrl.contains("mp3")) {
builder.setMimeType(MimeTypes.AUDIO_MPEG);
} else if (lowerUrl.contains(".ogg") || lowerUrl.contains("opus")) {
builder.setMimeType(MimeTypes.AUDIO_OGG);
}
return builder.build();
}
private static void runOnPlayerThread(Runnable runnable) {
mainHandler.post(runnable);
}
private static float clampVolume(float volume) {
if (volume < 0.0f) {
return 0.0f;
}
if (volume > 1.0f) {
return 1.0f;
}
return volume;
}
}
@@ -1,36 +0,0 @@
using System.IO;
using UnrealBuildTool;
public class AndroidRadioPlayer : ModuleRules
{
public AndroidRadioPlayer(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"Media",
"MediaAssets"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Projects"
});
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"ApplicationCore",
"Launch"
});
string PluginPath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath);
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "Android", "AndroidRadioPlayer_UPL.xml"));
}
}
}
@@ -1,207 +0,0 @@
#include "AndroidRadioJavaBridge.h"
#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#include "Android/AndroidJavaEnv.h"
#include "Android/AndroidJNI.h"
#endif
namespace AndroidRadioJavaBridge
{
#if PLATFORM_ANDROID
jclass GetJavaClass(JNIEnv* Env)
{
static jclass RadioPlayerClass = FAndroidApplication::FindJavaClassGlobalRef("com/epicgames/unreal/AndroidRadioPlayer");
if (RadioPlayerClass == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java class was not found."));
}
return RadioPlayerClass;
}
jmethodID GetStaticMethod(JNIEnv* Env, const ANSICHAR* MethodName, const ANSICHAR* Signature)
{
jclass RadioPlayerClass = GetJavaClass(Env);
if (RadioPlayerClass == nullptr)
{
return nullptr;
}
jmethodID Method = FJavaWrapper::FindStaticMethod(Env, RadioPlayerClass, MethodName, Signature, false);
if (Method == nullptr)
{
FAndroidApplication::CheckJavaException();
UE_LOG(LogTemp, Error, TEXT("AndroidRadioPlayer Java method was not found: %s"), ANSI_TO_TCHAR(MethodName));
}
return Method;
}
#endif
bool Play(const FString& Url, float Volume)
{
#if PLATFORM_ANDROID
if (Url.IsEmpty())
{
return false;
}
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
if (Env == nullptr)
{
return false;
}
jclass RadioPlayerClass = GetJavaClass(Env);
jmethodID Method = GetStaticMethod(Env, "playRadio", "(Landroid/app/Activity;Ljava/lang/String;F)Z");
if (RadioPlayerClass == nullptr || Method == nullptr)
{
return false;
}
auto JavaUrl = FJavaHelper::ToJavaString(Env, Url);
const jfloat ClampedVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method, FJavaWrapper::GameActivityThis, *JavaUrl, ClampedVolume);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
#else
return false;
#endif
}
void Stop()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "stopRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void Pause()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "pauseRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void Resume()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "resumeRadio", "()V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
}
#endif
}
void SetVolume(float Volume)
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "setRadioVolume", "(F)V") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
Env->CallStaticVoidMethod(RadioPlayerClass, Method, FMath::Clamp(Volume, 0.0f, 1.0f));
FAndroidApplication::CheckJavaException();
}
#endif
}
bool IsPlaying()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "isRadioPlaying", "()Z") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jboolean Result = Env->CallStaticBooleanMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return Result == JNI_TRUE;
}
#endif
return false;
}
int32 GetPlaybackState()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getPlaybackState", "()I") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jint State = Env->CallStaticIntMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int32>(State);
}
#endif
return -1;
}
FString GetLastError()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getLastError", "()Ljava/lang/String;") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
jstring Error = static_cast<jstring>(Env->CallStaticObjectMethod(RadioPlayerClass, Method));
FAndroidApplication::CheckJavaException();
return FJavaHelper::FStringFromLocalRef(Env, Error);
}
#endif
return FString();
}
int64 GetDurationMillis()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getDurationMillis", "()J") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jlong Duration = Env->CallStaticLongMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int64>(Duration);
}
#endif
return 0;
}
int64 GetCurrentPositionMillis()
{
#if PLATFORM_ANDROID
JNIEnv* Env = FAndroidApplication::GetJavaEnv();
jclass RadioPlayerClass = Env ? GetJavaClass(Env) : nullptr;
jmethodID Method = Env ? GetStaticMethod(Env, "getCurrentPositionMillis", "()J") : nullptr;
if (RadioPlayerClass != nullptr && Method != nullptr)
{
const jlong Position = Env->CallStaticLongMethod(RadioPlayerClass, Method);
FAndroidApplication::CheckJavaException();
return static_cast<int64>(Position);
}
#endif
return 0;
}
}
@@ -1,17 +0,0 @@
#pragma once
#include "CoreMinimal.h"
namespace AndroidRadioJavaBridge
{
bool Play(const FString& Url, float Volume);
void Stop();
void Pause();
void Resume();
void SetVolume(float Volume);
bool IsPlaying();
int32 GetPlaybackState();
FString GetLastError();
int64 GetDurationMillis();
int64 GetCurrentPositionMillis();
}
@@ -1,365 +0,0 @@
#include "AndroidRadioMediaPlayer.h"
#include "AndroidRadioJavaBridge.h"
#include "IMediaEventSink.h"
#include "Misc/Guid.h"
namespace
{
constexpr int32 JavaStateIdle = 0;
constexpr int32 JavaStateBuffering = 1;
constexpr int32 JavaStateReady = 2;
constexpr int32 JavaStateEnded = 3;
constexpr int32 JavaStateError = 4;
}
FAndroidRadioMediaPlayer::FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink)
: EventSink(InEventSink)
, CurrentState(EMediaState::Closed)
, CurrentStatus(EMediaStatus::None)
, CurrentRate(0.0f)
, CurrentVolume(1.0f)
, bLooping(false)
, bHasSentOpenEvent(false)
, bHasSentBufferingEvent(false)
, bHasSentEndEvent(false)
{
}
FAndroidRadioMediaPlayer::~FAndroidRadioMediaPlayer()
{
Close();
}
void FAndroidRadioMediaPlayer::Close()
{
if (CurrentState != EMediaState::Closed)
{
AndroidRadioJavaBridge::Stop();
CurrentUrl.Empty();
CurrentState = EMediaState::Closed;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
bHasSentOpenEvent = false;
bHasSentBufferingEvent = false;
bHasSentEndEvent = false;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaClosed);
}
}
IMediaCache& FAndroidRadioMediaPlayer::GetCache()
{
return *this;
}
IMediaControls& FAndroidRadioMediaPlayer::GetControls()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetInfo() const
{
return FString::Printf(TEXT("Android Radio Player\nUrl: %s\nState: %d\nLast error: %s"), *CurrentUrl, static_cast<int32>(CurrentState), *AndroidRadioJavaBridge::GetLastError());
}
FGuid FAndroidRadioMediaPlayer::GetPlayerPluginGUID() const
{
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
return PlayerPluginGUID;
}
IMediaSamples& FAndroidRadioMediaPlayer::GetSamples()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetStats() const
{
return FString();
}
IMediaTracks& FAndroidRadioMediaPlayer::GetTracks()
{
return *this;
}
FString FAndroidRadioMediaPlayer::GetUrl() const
{
return CurrentUrl;
}
IMediaView& FAndroidRadioMediaPlayer::GetView()
{
return *this;
}
bool FAndroidRadioMediaPlayer::Open(const FString& Url, const IMediaOptions* Options)
{
Close();
if (!Url.StartsWith(TEXT("http://")) && !Url.StartsWith(TEXT("https://")))
{
CurrentState = EMediaState::Error;
return false;
}
CurrentUrl = Url;
CurrentState = EMediaState::Preparing;
CurrentStatus = EMediaStatus::Connecting | EMediaStatus::Buffering;
CurrentRate = 1.0f;
bHasSentOpenEvent = false;
bHasSentBufferingEvent = false;
bHasSentEndEvent = false;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaConnecting);
if (!AndroidRadioJavaBridge::Play(Url, CurrentVolume))
{
CurrentState = EMediaState::Error;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpenFailed);
return false;
}
return true;
}
bool FAndroidRadioMediaPlayer::Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options)
{
return false;
}
void FAndroidRadioMediaPlayer::TickInput(FTimespan DeltaTime, FTimespan Timecode)
{
UpdateStateFromJava();
}
bool FAndroidRadioMediaPlayer::SetNativeVolume(float Volume)
{
CurrentVolume = FMath::Clamp(Volume, 0.0f, 1.0f);
AndroidRadioJavaBridge::SetVolume(CurrentVolume);
return true;
}
bool FAndroidRadioMediaPlayer::CanControl(EMediaControl Control) const
{
return Control == EMediaControl::Pause || Control == EMediaControl::Resume;
}
FTimespan FAndroidRadioMediaPlayer::GetDuration() const
{
const int64 DurationMillis = AndroidRadioJavaBridge::GetDurationMillis();
return DurationMillis > 0 ? FTimespan::FromMilliseconds(DurationMillis) : FTimespan::Zero();
}
float FAndroidRadioMediaPlayer::GetRate() const
{
return CurrentRate;
}
EMediaState FAndroidRadioMediaPlayer::GetState() const
{
return CurrentState;
}
EMediaStatus FAndroidRadioMediaPlayer::GetStatus() const
{
return CurrentStatus;
}
TRangeSet<float> FAndroidRadioMediaPlayer::GetSupportedRates(EMediaRateThinning Thinning) const
{
TRangeSet<float> Result;
Result.Add(TRange<float>(0.0f));
Result.Add(TRange<float>(1.0f));
return Result;
}
FTimespan FAndroidRadioMediaPlayer::GetTime() const
{
return FTimespan::FromMilliseconds(AndroidRadioJavaBridge::GetCurrentPositionMillis());
}
bool FAndroidRadioMediaPlayer::IsLooping() const
{
return bLooping;
}
bool FAndroidRadioMediaPlayer::Seek(const FTimespan& Time)
{
return false;
}
bool FAndroidRadioMediaPlayer::SetLooping(bool Looping)
{
bLooping = Looping;
return true;
}
bool FAndroidRadioMediaPlayer::SetRate(float Rate)
{
if (CurrentState == EMediaState::Closed || CurrentState == EMediaState::Error)
{
return false;
}
if (FMath::IsNearlyZero(Rate))
{
AndroidRadioJavaBridge::Pause();
CurrentRate = 0.0f;
CurrentState = EMediaState::Paused;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackSuspended);
return true;
}
if (FMath::IsNearlyEqual(Rate, 1.0f))
{
AndroidRadioJavaBridge::Resume();
CurrentRate = 1.0f;
CurrentState = EMediaState::Playing;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackResumed);
return true;
}
return false;
}
bool FAndroidRadioMediaPlayer::GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const
{
if (TrackIndex == 0 && FormatIndex == 0)
{
OutFormat.BitsPerSample = 16;
OutFormat.NumChannels = 2;
OutFormat.SampleRate = 44100;
OutFormat.TypeName = TEXT("External");
return true;
}
return false;
}
int32 FAndroidRadioMediaPlayer::GetNumTracks(EMediaTrackType TrackType) const
{
return TrackType == EMediaTrackType::Audio ? 1 : 0;
}
int32 FAndroidRadioMediaPlayer::GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 1 : 0;
}
int32 FAndroidRadioMediaPlayer::GetSelectedTrack(EMediaTrackType TrackType) const
{
return TrackType == EMediaTrackType::Audio ? 0 : INDEX_NONE;
}
FText FAndroidRadioMediaPlayer::GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? NSLOCTEXT("AndroidRadioMediaPlayer", "AudioTrackName", "Radio") : FText::GetEmpty();
}
int32 FAndroidRadioMediaPlayer::GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? 0 : INDEX_NONE;
}
FString FAndroidRadioMediaPlayer::GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TEXT("und");
}
FString FAndroidRadioMediaPlayer::GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 ? TEXT("Radio") : FString();
}
bool FAndroidRadioMediaPlayer::GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const
{
return false;
}
bool FAndroidRadioMediaPlayer::SelectTrack(EMediaTrackType TrackType, int32 TrackIndex)
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0;
}
bool FAndroidRadioMediaPlayer::SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex)
{
return TrackType == EMediaTrackType::Audio && TrackIndex == 0 && FormatIndex == 0;
}
bool FAndroidRadioMediaPlayer::PeekVideoSampleTime(FMediaTimeStamp& TimeStamp)
{
return false;
}
void FAndroidRadioMediaPlayer::UpdateStateFromJava()
{
if (CurrentState == EMediaState::Closed || CurrentUrl.IsEmpty())
{
return;
}
const int32 JavaState = AndroidRadioJavaBridge::GetPlaybackState();
const bool bIsPlaying = AndroidRadioJavaBridge::IsPlaying();
if (JavaState == JavaStateError)
{
if (CurrentState != EMediaState::Error)
{
CurrentState = EMediaState::Error;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
EventSink.ReceiveMediaEvent(bHasSentOpenEvent ? EMediaEvent::PlaybackSuspended : EMediaEvent::MediaOpenFailed);
}
return;
}
if (JavaState == JavaStateBuffering)
{
CurrentState = EMediaState::Preparing;
CurrentStatus = EMediaStatus::Buffering;
if (!bHasSentBufferingEvent)
{
bHasSentBufferingEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBuffering);
}
return;
}
if (JavaState == JavaStateReady)
{
if (!bHasSentOpenEvent)
{
bHasSentOpenEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::MediaOpened);
EventSink.ReceiveMediaEvent(EMediaEvent::TracksChanged);
EventSink.ReceiveMediaEvent(EMediaEvent::MediaBufferingComplete);
}
CurrentStatus = EMediaStatus::None;
CurrentState = bIsPlaying ? EMediaState::Playing : EMediaState::Paused;
CurrentRate = bIsPlaying ? 1.0f : 0.0f;
bHasSentBufferingEvent = false;
return;
}
if (JavaState == JavaStateEnded)
{
CurrentState = EMediaState::Stopped;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
if (!bHasSentEndEvent)
{
bHasSentEndEvent = true;
EventSink.ReceiveMediaEvent(EMediaEvent::PlaybackEndReached);
}
return;
}
if (JavaState == JavaStateIdle)
{
CurrentState = CurrentUrl.IsEmpty() ? EMediaState::Closed : EMediaState::Stopped;
CurrentStatus = EMediaStatus::None;
CurrentRate = 0.0f;
}
}
@@ -1,78 +0,0 @@
#pragma once
#include "IMediaCache.h"
#include "IMediaControls.h"
#include "IMediaPlayer.h"
#include "IMediaSamples.h"
#include "IMediaTracks.h"
#include "IMediaView.h"
class IMediaEventSink;
class FAndroidRadioMediaPlayer
: public IMediaPlayer
, protected IMediaCache
, protected IMediaControls
, protected IMediaSamples
, protected IMediaTracks
, protected IMediaView
{
public:
explicit FAndroidRadioMediaPlayer(IMediaEventSink& InEventSink);
virtual ~FAndroidRadioMediaPlayer() override;
virtual void Close() override;
virtual IMediaCache& GetCache() override;
virtual IMediaControls& GetControls() override;
virtual FString GetInfo() const override;
virtual FGuid GetPlayerPluginGUID() const override;
virtual IMediaSamples& GetSamples() override;
virtual FString GetStats() const override;
virtual IMediaTracks& GetTracks() override;
virtual FString GetUrl() const override;
virtual IMediaView& GetView() override;
virtual bool Open(const FString& Url, const IMediaOptions* Options) override;
virtual bool Open(const TSharedRef<FArchive, ESPMode::ThreadSafe>& Archive, const FString& OriginalUrl, const IMediaOptions* Options) override;
virtual void TickInput(FTimespan DeltaTime, FTimespan Timecode) override;
virtual bool SetNativeVolume(float Volume) override;
virtual bool CanControl(EMediaControl Control) const override;
virtual FTimespan GetDuration() const override;
virtual float GetRate() const override;
virtual EMediaState GetState() const override;
virtual EMediaStatus GetStatus() const override;
virtual TRangeSet<float> GetSupportedRates(EMediaRateThinning Thinning) const override;
virtual FTimespan GetTime() const override;
virtual bool IsLooping() const override;
virtual bool Seek(const FTimespan& Time) override;
virtual bool SetLooping(bool Looping) override;
virtual bool SetRate(float Rate) override;
virtual bool GetAudioTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaAudioTrackFormat& OutFormat) const override;
virtual int32 GetNumTracks(EMediaTrackType TrackType) const override;
virtual int32 GetNumTrackFormats(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual int32 GetSelectedTrack(EMediaTrackType TrackType) const override;
virtual FText GetTrackDisplayName(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual int32 GetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual FString GetTrackLanguage(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual FString GetTrackName(EMediaTrackType TrackType, int32 TrackIndex) const override;
virtual bool GetVideoTrackFormat(int32 TrackIndex, int32 FormatIndex, FMediaVideoTrackFormat& OutFormat) const override;
virtual bool SelectTrack(EMediaTrackType TrackType, int32 TrackIndex) override;
virtual bool SetTrackFormat(EMediaTrackType TrackType, int32 TrackIndex, int32 FormatIndex) override;
virtual bool PeekVideoSampleTime(FMediaTimeStamp& TimeStamp) override;
private:
void UpdateStateFromJava();
IMediaEventSink& EventSink;
FString CurrentUrl;
EMediaState CurrentState;
EMediaStatus CurrentStatus;
float CurrentRate;
float CurrentVolume;
bool bLooping;
bool bHasSentOpenEvent;
bool bHasSentBufferingEvent;
bool bHasSentEndEvent;
};
@@ -1,74 +0,0 @@
#include "AndroidRadioPlayerBPLibrary.h"
#include "AndroidRadioJavaBridge.h"
bool UAndroidRadioPlayerBPLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
{
if (MediaPlayer == nullptr || StreamMediaSource == nullptr || !Url.Contains(TEXT("://")))
{
return false;
}
StreamMediaSource->StreamUrl = Url;
MediaPlayer->Close();
return MediaPlayer->OpenSource(StreamMediaSource);
}
bool UAndroidRadioPlayerBPLibrary::PlayRadio(const FString& Url, float Volume)
{
return AndroidRadioJavaBridge::Play(Url, Volume);
}
void UAndroidRadioPlayerBPLibrary::StopRadio()
{
AndroidRadioJavaBridge::Stop();
}
void UAndroidRadioPlayerBPLibrary::PauseRadio()
{
AndroidRadioJavaBridge::Pause();
}
void UAndroidRadioPlayerBPLibrary::ResumeRadio()
{
AndroidRadioJavaBridge::Resume();
}
void UAndroidRadioPlayerBPLibrary::SetRadioVolume(float Volume)
{
AndroidRadioJavaBridge::SetVolume(Volume);
}
bool UAndroidRadioPlayerBPLibrary::IsRadioPlaying()
{
return AndroidRadioJavaBridge::IsPlaying();
}
EAndroidRadioPlaybackState UAndroidRadioPlayerBPLibrary::GetRadioPlaybackState()
{
switch (AndroidRadioJavaBridge::GetPlaybackState())
{
case 0:
return EAndroidRadioPlaybackState::Idle;
case 1:
return EAndroidRadioPlaybackState::Buffering;
case 2:
return EAndroidRadioPlaybackState::Ready;
case 3:
return EAndroidRadioPlaybackState::Ended;
case 4:
return EAndroidRadioPlaybackState::Error;
default:
return EAndroidRadioPlaybackState::Unsupported;
}
}
FString UAndroidRadioPlayerBPLibrary::GetRadioLastError()
{
return AndroidRadioJavaBridge::GetLastError();
}
bool UAndroidRadioPlayerBPLibrary::IsAndroidRadioPlayerSupported()
{
return PLATFORM_ANDROID;
}
@@ -1,115 +0,0 @@
#include "AndroidRadioMediaPlayer.h"
#include "IMediaModule.h"
#include "IMediaPlayerFactory.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "FAndroidRadioPlayerModule"
class FAndroidRadioPlayerModule
: public IModuleInterface
, public IMediaPlayerFactory
{
public:
virtual void StartupModule() override
{
SupportedPlatforms.Add(TEXT("Android"));
SupportedUriSchemes.Add(TEXT("http"));
SupportedUriSchemes.Add(TEXT("https"));
if (IMediaModule* MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media"))
{
MediaModule->RegisterPlayerFactory(*this);
}
}
virtual void ShutdownModule() override
{
if (IMediaModule* MediaModule = FModuleManager::GetModulePtr<IMediaModule>("Media"))
{
MediaModule->UnregisterPlayerFactory(*this);
}
}
virtual bool CanPlayUrl(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
{
return GetPlayabilityConfidenceScore(Url, Options, OutWarnings, OutErrors) > 0;
}
virtual int32 GetPlayabilityConfidenceScore(const FString& Url, const IMediaOptions* Options, TArray<FText>* OutWarnings, TArray<FText>* OutErrors) const override
{
FString Scheme;
FString Location;
if (!Url.Split(TEXT("://"), &Scheme, &Location, ESearchCase::CaseSensitive))
{
if (OutErrors != nullptr)
{
OutErrors->Add(LOCTEXT("NoSchemeFound", "No URI scheme found"));
}
return 0;
}
if (!SupportedUriSchemes.Contains(Scheme))
{
if (OutErrors != nullptr)
{
OutErrors->Add(FText::Format(LOCTEXT("SchemeNotSupported", "The URI scheme '{0}' is not supported"), FText::FromString(Scheme)));
}
return 0;
}
const FString Extension = FPaths::GetExtension(Location, false).ToLower();
if (Extension.IsEmpty() || Extension == TEXT("mp3") || Extension == TEXT("aac") || Extension == TEXT("m3u8") || Extension == TEXT("pls") || Extension == TEXT("ogg") || Extension == TEXT("opus"))
{
return 90;
}
if (OutWarnings != nullptr)
{
OutWarnings->Add(FText::Format(LOCTEXT("ExtensionProbablyNotRadio", "The file extension '{0}' is not a known radio stream extension"), FText::FromString(Extension)));
}
return 20;
}
virtual TSharedPtr<IMediaPlayer, ESPMode::ThreadSafe> CreatePlayer(IMediaEventSink& EventSink) override
{
return MakeShared<FAndroidRadioMediaPlayer, ESPMode::ThreadSafe>(EventSink);
}
virtual FText GetDisplayName() const override
{
return LOCTEXT("MediaPlayerDisplayName", "Android Radio Player");
}
virtual FName GetPlayerName() const override
{
static FName PlayerName(TEXT("AndroidRadioPlayer"));
return PlayerName;
}
virtual FGuid GetPlayerPluginGUID() const override
{
static FGuid PlayerPluginGUID(0x2547a7af, 0xe9504c2f, 0xa090e1b7, 0x26c7d716);
return PlayerPluginGUID;
}
virtual const TArray<FString>& GetSupportedPlatforms() const override
{
return SupportedPlatforms;
}
virtual bool SupportsFeature(EMediaFeature Feature) const override
{
return Feature == EMediaFeature::AudioTracks;
}
private:
TArray<FString> SupportedPlatforms;
TArray<FString> SupportedUriSchemes;
};
IMPLEMENT_MODULE(FAndroidRadioPlayerModule, AndroidRadioPlayer)
#undef LOCTEXT_NAMESPACE
@@ -1,54 +0,0 @@
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MediaPlayer.h"
#include "StreamMediaSource.h"
#include "AndroidRadioPlayerBPLibrary.generated.h"
UENUM(BlueprintType)
enum class EAndroidRadioPlaybackState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Buffering UMETA(DisplayName = "Buffering"),
Ready UMETA(DisplayName = "Ready"),
Ended UMETA(DisplayName = "Ended"),
Error UMETA(DisplayName = "Error"),
Unsupported UMETA(DisplayName = "Unsupported")
};
UCLASS()
class ANDROIDRADIOPLAYER_API UAndroidRadioPlayerBPLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Android Radio Player|Media")
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static bool PlayRadio(const FString& Url, float Volume = 1.0f);
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void StopRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void PauseRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void ResumeRadio();
UFUNCTION(BlueprintCallable, Category = "Android Radio Player")
static void SetRadioVolume(float Volume);
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static bool IsRadioPlaying();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static EAndroidRadioPlaybackState GetRadioPlaybackState();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static FString GetRadioLastError();
UFUNCTION(BlueprintPure, Category = "Android Radio Player")
static bool IsAndroidRadioPlayerSupported();
};
+1 -1
View File
@@ -8,7 +8,7 @@ public class FunnyGirl : ModuleRules
{ {
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "UMG" }); PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "MediaAssets", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
@@ -169,6 +169,87 @@ bool UPersonaBrainComponent::GetLastSavedLLMMessage(FString& OutMessage) const
return false; return false;
} }
TArray<FPersonaChatMessage> UPersonaBrainComponent::GetSavedChatMessages() const
{
TArray<FPersonaChatMessage> ChatMessages;
const FString FilePath = GetChatFilePath();
FString ChatJson;
if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
{
return ChatMessages;
}
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ChatJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
return ChatMessages;
}
const TArray<TSharedPtr<FJsonValue>>* SavedMessages = nullptr;
if (!RootObject->TryGetArrayField(TEXT("messages"), SavedMessages) || !SavedMessages)
{
return ChatMessages;
}
const FString GirlSpeaker = Identity && !Identity->Name.IsEmpty() && !Identity->Name.ToString().TrimStartAndEnd().IsEmpty()
? Identity->Name.ToString().TrimStartAndEnd()
: FString(TEXT("Girl"));
for (const TSharedPtr<FJsonValue>& MessageValue : *SavedMessages)
{
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
if (!MessageValue.IsValid() || !MessageValue->TryGetObject(MessageObject) || !MessageObject || !MessageObject->IsValid())
{
continue;
}
FString Role;
FString Content;
if (!(*MessageObject)->TryGetStringField(TEXT("role"), Role) || !(*MessageObject)->TryGetStringField(TEXT("content"), Content))
{
continue;
}
if ((Role != TEXT("user") && Role != TEXT("assistant")) || Content.TrimStartAndEnd().IsEmpty())
{
continue;
}
FPersonaChatMessage ChatMessage;
(*MessageObject)->TryGetStringField(TEXT("timestamp"), ChatMessage.Timestamp);
ChatMessage.Role = Role;
ChatMessage.Speaker = Role == TEXT("assistant") ? GirlSpeaker : FString(TEXT("Player"));
ChatMessage.Content = Content;
const TSharedPtr<FJsonValue> InteractActionValue = (*MessageObject)->TryGetField(TEXT("interact_action"));
if (InteractActionValue.IsValid() && InteractActionValue->Type != EJson::Null)
{
if (InteractActionValue->Type == EJson::Object)
{
const TSharedPtr<FJsonObject>* InteractActionObject = nullptr;
if (InteractActionValue->TryGetObject(InteractActionObject) && InteractActionObject && InteractActionObject->IsValid())
{
FString InteractActionJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&InteractActionJson);
FJsonSerializer::Serialize(InteractActionObject->ToSharedRef(), Writer);
ChatMessage.InteractActionJson = InteractActionJson;
}
}
else
{
InteractActionValue->TryGetString(ChatMessage.InteractActionJson);
}
}
(*MessageObject)->TryGetStringField(TEXT("emotion"), ChatMessage.Emotion);
ChatMessages.Add(ChatMessage);
}
return ChatMessages;
}
void UPersonaBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const void UPersonaBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const
{ {
OnLLMError.Broadcast(ErrorMessage); OnLLMError.Broadcast(ErrorMessage);
@@ -2,8 +2,6 @@
#include "Morph/MorphControllerComponent.h" #include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h" #include "Morph/MorphPresetDataAsset.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
#include "Components/SkeletalMeshComponent.h" #include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Character.h" #include "GameFramework/Character.h"
#include "Engine/World.h" #include "Engine/World.h"
@@ -37,6 +35,12 @@ void UMorphControllerComponent::BeginPlay()
Super::BeginPlay(); Super::BeginPlay();
} }
void UMorphControllerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
ClearMorphsImmediately();
Super::EndPlay(EndPlayReason);
}
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{ {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
@@ -212,7 +216,6 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
if (Slot.Preset == Preset) if (Slot.Preset == Preset)
{ {
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f); Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
Slot.CurrentWeight = Slot.TargetWeight;
Slot.PlaybackTime = 0.0f; Slot.PlaybackTime = 0.0f;
Slot.LoopDelayRemaining = 0.0f; Slot.LoopDelayRemaining = 0.0f;
Slot.TimelineDuration = TimelineDuration; Slot.TimelineDuration = TimelineDuration;
@@ -224,7 +227,7 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
FActiveMorphPresetSlot NewSlot; FActiveMorphPresetSlot NewSlot;
NewSlot.Preset = Preset; NewSlot.Preset = Preset;
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f); NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
NewSlot.CurrentWeight = NewSlot.TargetWeight; NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f; NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f; NewSlot.LoopDelayRemaining = 0.0f;
NewSlot.TimelineDuration = TimelineDuration; NewSlot.TimelineDuration = TimelineDuration;
@@ -239,78 +242,32 @@ bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float
{ {
if (bLogWarnings) if (bLogWarnings)
{ {
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips or animation montage."), *GetNameSafe(GetOwner())); UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips."), *GetNameSafe(GetOwner()));
} }
return false; return false;
} }
const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false; return AddPreset(Preset, Weight);
const bool bPlayedAnimation = PlayPresetAnimation(Preset);
return bPlayedMorphs || bPlayedAnimation;
}
bool UMorphControllerComponent::PlayPresetAnimation(UMorphPresetDataAsset* Preset)
{
if (!Preset || !Preset->AnimationMontage)
{
return false;
}
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (!Mesh)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
}
return false;
}
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
if (!AnimInstance)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
}
return false;
}
UAnimMontage* Montage = Preset->AnimationMontage;
if (Preset->bRestartAnimationOnReplay && AnimInstance->Montage_IsPlaying(Montage))
{
AnimInstance->Montage_Stop(0.0f, Montage);
}
const float MontageLength = AnimInstance->Montage_Play(Montage, FMath::Max(Preset->AnimationPlayRate, 0.01f));
if (MontageLength <= 0.0f)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation failed on %s: montage %s could not play. Check the Anim Blueprint slot setup."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
}
return false;
}
if (!Preset->AnimationStartSection.IsNone())
{
AnimInstance->Montage_JumpToSection(Preset->AnimationStartSection, Montage);
}
return true;
} }
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset) void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
{ {
if (!Preset) return; if (!Preset) return;
bool bRemovedAny = false;
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i) for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
{ {
if (ActivePresets[i].Preset == Preset) if (ActivePresets[i].Preset == Preset)
{ {
ActivePresets.RemoveAt(i); ActivePresets.RemoveAt(i);
bRemovedAny = true;
} }
} }
if (bRemovedAny)
{
UpdateMorphTargets(0.0f);
}
} }
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight) void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
@@ -336,6 +293,7 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
if (Slot.Preset && Slot.Preset->IsValidPreset()) if (Slot.Preset && Slot.Preset->IsValidPreset())
{ {
FActiveMorphPresetSlot NewSlot = Slot; FActiveMorphPresetSlot NewSlot = Slot;
NewSlot.TargetWeight = FMath::Clamp(NewSlot.TargetWeight, 0.0f, 1.0f);
NewSlot.CurrentWeight = 0.0f; NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f; NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f; NewSlot.LoopDelayRemaining = 0.0f;
@@ -346,11 +304,14 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
} }
} }
} }
UpdateMorphTargets(0.0f);
} }
void UMorphControllerComponent::ClearAllPresets() void UMorphControllerComponent::ClearAllPresets()
{ {
ActivePresets.Empty(); ActivePresets.Empty();
UpdateMorphTargets(0.0f);
} }
bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const
@@ -382,17 +343,27 @@ TArray<FActiveMorphPresetSlot> UMorphControllerComponent::GetActivePresets() con
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value) void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
{ {
if (MorphTargetName.IsNone())
{
return;
}
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value)); DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
UpdateMorphTargets(0.0f);
} }
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName) void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
{ {
DirectMorphOverrides.Remove(MorphTargetName); if (DirectMorphOverrides.Remove(MorphTargetName) > 0)
{
UpdateMorphTargets(0.0f);
}
} }
void UMorphControllerComponent::ClearDirectMorphOverrides() void UMorphControllerComponent::ClearDirectMorphOverrides()
{ {
DirectMorphOverrides.Empty(); DirectMorphOverrides.Empty();
UpdateMorphTargets(0.0f);
} }
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues) void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues)
@@ -405,11 +376,13 @@ void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>&
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value)); LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
} }
} }
UpdateMorphTargets(0.0f);
} }
void UMorphControllerComponent::ClearLipSyncMorphValues() void UMorphControllerComponent::ClearLipSyncMorphValues()
{ {
LipSyncMorphValues.Empty(); LipSyncMorphValues.Empty();
UpdateMorphTargets(0.0f);
} }
void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime) void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime)
@@ -501,6 +474,19 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
// 2. Gather final morph values. Overlapping clips use the strongest contribution. // 2. Gather final morph values. Overlapping clips use the strongest contribution.
TMap<FName, float> FinalValues; TMap<FName, float> FinalValues;
float ActivePresetWeightSum = 0.0f;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset && Slot.CurrentWeight >= 0.001f && Slot.LoopDelayRemaining <= 0.0f)
{
ActivePresetWeightSum += Slot.CurrentWeight;
}
}
const float PresetLayerScale = bNormalizePresetLayerWeights && MaxCombinedPresetWeight > 0.0f && ActivePresetWeightSum > MaxCombinedPresetWeight
? MaxCombinedPresetWeight / ActivePresetWeightSum
: 1.0f;
for (const FActiveMorphPresetSlot& Slot : ActivePresets) for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{ {
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f) if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
@@ -508,7 +494,7 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
continue; continue;
} }
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight, FinalValues); MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight * PresetLayerScale, FinalValues);
} }
// 3. Apply direct overrides with the same max-based priority rule. // 3. Apply direct overrides with the same max-based priority rule.
@@ -9,11 +9,13 @@
#include "Engine/SkeletalMesh.h" #include "Engine/SkeletalMesh.h"
#include "Engine/Texture2D.h" #include "Engine/Texture2D.h"
#include "HAL/FileManager.h" #include "HAL/FileManager.h"
#include "MediaPlayer.h"
#include "Misc/Base64.h" #include "Misc/Base64.h"
#include "Misc/FileHelper.h" #include "Misc/FileHelper.h"
#include "Misc/Paths.h" #include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h" #include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h" #include "Serialization/JsonWriter.h"
#include "StreamMediaSource.h"
namespace namespace
{ {
@@ -358,3 +360,15 @@ bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field,
SaveObject->RemoveField(EncodedField); SaveObject->RemoveField(EncodedField);
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError); return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
} }
bool UFunnyGirlUtilityBlueprintLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
{
if (!MediaPlayer || !StreamMediaSource || !Url.Contains(TEXT("://")))
{
return false;
}
StreamMediaSource->StreamUrl = Url;
MediaPlayer->Close();
return MediaPlayer->OpenSource(StreamMediaSource);
}
@@ -28,6 +28,30 @@ struct FUNNYGIRL_API FLLMReplyResponse
FString Emotion; FString Emotion;
}; };
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FPersonaChatMessage
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Timestamp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Role;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Speaker;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Content;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Interaction")
FString InteractActionJson;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Emotion")
FString Emotion;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
@@ -85,6 +109,9 @@ public:
UFUNCTION(BlueprintPure, Category = "LLM|History") UFUNCTION(BlueprintPure, Category = "LLM|History")
bool GetLastSavedLLMMessage(FString& OutMessage) const; bool GetLastSavedLLMMessage(FString& OutMessage) const;
UFUNCTION(BlueprintPure, Category = "LLM|History")
TArray<FPersonaChatMessage> GetSavedChatMessages() const;
virtual void CancelRequest() override; virtual void CancelRequest() override;
protected: protected:
@@ -74,6 +74,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
bool bLogWarnings = true; bool bLogWarnings = true;
/** Keep layered presets from overdriving the face when several expressions are active at once. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering")
bool bNormalizePresetLayerWeights = true;
/** Maximum total expression weight shared by active presets when normalization is enabled. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering", meta = (ClampMin = "0", ClampMax = "5"))
float MaxCombinedPresetWeight = 1.0f;
/** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */ /** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets; TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets;
@@ -178,6 +186,7 @@ public:
protected: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private: private:
UPROPERTY() UPROPERTY()
@@ -197,7 +206,6 @@ private:
USkeletalMeshComponent* ResolveMeshComponent() const; USkeletalMeshComponent* ResolveMeshComponent() const;
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const; bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
void UpdateMorphTargets(float DeltaTime); void UpdateMorphTargets(float DeltaTime);
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const; void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues); void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues);
@@ -8,8 +8,6 @@
#include "Engine/SkeletalMesh.h" #include "Engine/SkeletalMesh.h"
#include "MorphPresetDataAsset.generated.h" #include "MorphPresetDataAsset.generated.h"
class UAnimMontage;
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FUNNYGIRL_API FMorphTimelineClip struct FUNNYGIRL_API FMorphTimelineClip
{ {
@@ -77,26 +75,6 @@ public:
ClampMin = "0")) ClampMin = "0"))
float BlendSpeedOverride = 0.0f; float BlendSpeedOverride = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Montage",
ToolTip = "Optional animation montage played when this preset starts. The montage must use a slot that is present in the character Anim Blueprint."))
TObjectPtr<UAnimMontage> AnimationMontage = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Play Rate",
ClampMin = "0.01"))
float AnimationPlayRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Start Section",
ToolTip = "Optional montage section to jump to after playback starts."))
FName AnimationStartSection;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Restart Animation On Replay",
ToolTip = "If enabled, an already playing montage is stopped and restarted when this preset is triggered again."))
bool bRestartAnimationOnReplay = true;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
int32 GetMorphCount() const; int32 GetMorphCount() const;
@@ -104,10 +82,7 @@ public:
float GetTimelineLength() const; float GetTimelineLength() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasAnimation() const { return AnimationMontage != nullptr; } bool HasPlayableContent() const { return IsValidPreset(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool IsValidPreset() const { return GetMorphCount() > 0; } bool IsValidPreset() const { return GetMorphCount() > 0; }
@@ -8,6 +8,8 @@
class USkeletalMesh; class USkeletalMesh;
class UButton; class UButton;
class UMediaPlayer;
class UStreamMediaSource;
class UTexture2D; class UTexture2D;
UENUM(BlueprintType) UENUM(BlueprintType)
@@ -53,4 +55,7 @@ public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save") UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool DeleteLightString(const FString& Field, FString& OutError); static bool DeleteLightString(const FString& Field, FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Media")
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
}; };
@@ -64,10 +64,6 @@ namespace
TargetPreset.bLoop = SourcePreset.bLoop; TargetPreset.bLoop = SourcePreset.bLoop;
TargetPreset.LoopDelay = SourcePreset.LoopDelay; TargetPreset.LoopDelay = SourcePreset.LoopDelay;
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride; TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
} }
} }
@@ -4,10 +4,7 @@
#include "Animation/AnimData/IAnimationDataController.h" #include "Animation/AnimData/IAnimationDataController.h"
#include "AnimPreviewInstance.h" #include "AnimPreviewInstance.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h" #include "Animation/AnimSequence.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/AnimationAsset.h"
#include "Animation/DebugSkelMeshComponent.h" #include "Animation/DebugSkelMeshComponent.h"
#include "Animation/Skeleton.h" #include "Animation/Skeleton.h"
#include "Components/SkeletalMeshComponent.h" #include "Components/SkeletalMeshComponent.h"
@@ -115,15 +112,7 @@ float FMorphPresetEditorController::GetTimelineLength() const
{ {
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get()) if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{ {
float TimelineLength = PresetAsset->GetTimelineLength(); return FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
if (PresetAsset->AnimationMontage)
{
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
const UAnimSequenceBase* PreviewSequence = PresetAsset->AnimationMontage->GetFirstAnimReference();
const float AnimationLength = PreviewSequence ? PreviewSequence->GetPlayLength() : PresetAsset->AnimationMontage->GetPlayLength();
TimelineLength = FMath::Max(TimelineLength, AnimationLength / PlayRate);
}
return FMath::Max(TimelineLength, 0.1f);
} }
return 1.0f; return 1.0f;
@@ -145,7 +134,7 @@ void FMorphPresetEditorController::SetCurrentTime(float InTime)
if (PreviewMeshComponent->PreviewInstance) if (PreviewMeshComponent->PreviewInstance)
{ {
PreviewMeshComponent->PreviewInstance->SetPlaying(false); PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false); PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose(); RefreshPreviewAnimationPose();
} }
} }
@@ -225,20 +214,21 @@ void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChang
PresetAsset->MarkPackageDirty(); PresetAsset->MarkPackageDirty();
} }
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset(); if (ActivePreviewMesh.Get() != GetPreviewMesh())
UAnimationAsset* CurrentPreviewAsset = nullptr;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{ {
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset(); RebuildPreviewScene();
if (bBroadcastPresetChanged)
{
PresetChanged.Broadcast();
}
return;
} }
if (DesiredPreviewAsset != CurrentPreviewAsset || if (!PreviewAnimation.IsValid())
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
{ {
RebuildPreviewAnimation(); RebuildPreviewAnimation();
} }
else if (PreviewAnimation.IsValid()) else
{ {
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false); SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
} }
@@ -436,6 +426,7 @@ void FMorphPresetEditorController::RebuildPreviewScene()
PreviewAnimation.Reset(); PreviewAnimation.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh(); USkeletalMesh* PreviewMesh = GetPreviewMesh();
ActivePreviewMesh = PreviewMesh;
if (PreviewMesh == nullptr) if (PreviewMesh == nullptr)
{ {
PreviewSceneChanged.Broadcast(); PreviewSceneChanged.Broadcast();
@@ -464,56 +455,29 @@ void FMorphPresetEditorController::RebuildPreviewAnimation()
PreviewAnimation.Reset(); PreviewAnimation.Reset();
PreviewStatusText = FText::GetEmpty(); PreviewStatusText = FText::GetEmpty();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset(); PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset)) SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
{ {
const USkeletalMesh* PreviewMesh = GetPreviewMesh(); if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
const FString PreviewMeshSkeletonName = PreviewMesh && PreviewMesh->GetSkeleton()
? PreviewMesh->GetSkeleton()->GetPathName()
: TEXT("None");
const FString AnimationSkeletonName = PreviewAsset->GetSkeleton()
? PreviewAsset->GetSkeleton()->GetPathName()
: TEXT("None");
PreviewStatusText = FText::FromString(FString::Printf(
TEXT("Animation preview is disabled: montage sequence '%s' uses skeleton '%s', but Preview Mesh uses '%s'. Retarget the animation to the Preview Mesh skeleton."),
*GetNameSafe(PreviewAsset),
*AnimationSkeletonName,
*PreviewMeshSkeletonName));
UE_LOG(LogMorphPresetEditorPreview, Warning, TEXT("%s"), *PreviewStatusText.ToString());
PreviewAsset = nullptr;
}
if (!PreviewAsset)
{
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
PreviewAsset = AnimSequence;
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
{ {
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton()) AnimSequence->SetSkeleton(Skeleton);
{
AnimSequence->SetSkeleton(Skeleton);
}
} }
} }
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true); PreviewScene->SetPreviewAnimationAsset(AnimSequence, true);
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent()) if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{ {
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode); PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
if (PreviewMeshComponent->PreviewInstance) if (PreviewMeshComponent->PreviewInstance)
{ {
const UMorphPresetDataAsset* PresetAsset = Preset.Get(); PreviewMeshComponent->PreviewInstance->SetAnimationAsset(AnimSequence, false, 1.0f);
const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
PreviewMeshComponent->PreviewInstance->EnableControllers(true); PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f); PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
PreviewMeshComponent->PreviewInstance->SetPlaying(false); PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetLooping(false); PreviewMeshComponent->PreviewInstance->SetLooping(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false); PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose(); RefreshPreviewAnimationPose();
} }
} }
@@ -539,52 +503,6 @@ void FMorphPresetEditorController::RefreshPreviewAnimationPose()
PreviewScene->InvalidateViews(); PreviewScene->InvalidateViews();
} }
UAnimationAsset* FMorphPresetEditorController::ResolvePreviewAnimationAsset() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (PresetAsset && PresetAsset->AnimationMontage)
{
if (UAnimSequenceBase* FirstAnimReference = PresetAsset->AnimationMontage->GetFirstAnimReference())
{
return FirstAnimReference;
}
}
return nullptr;
}
float FMorphPresetEditorController::GetPreviewAnimationPosition() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (!PresetAsset || !PreviewAsset)
{
return CurrentTime;
}
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
float PreviewPosition = CurrentTime * PlayRate;
if (const UAnimSequenceBase* PreviewSequence = Cast<UAnimSequenceBase>(PreviewAsset))
{
return FMath::Clamp(PreviewPosition, 0.0f, PreviewSequence->GetPlayLength());
}
return PreviewPosition;
}
bool FMorphPresetEditorController::IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const
{
if (!PreviewAsset)
{
return true;
}
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
const USkeleton* PreviewSkeleton = PreviewMesh ? PreviewMesh->GetSkeleton() : nullptr;
const USkeleton* AnimationSkeleton = PreviewAsset->GetSkeleton();
return PreviewSkeleton && AnimationSkeleton && PreviewSkeleton->IsCompatibleForEditor(AnimationSkeleton);
}
void FMorphPresetEditorController::ApplyPresetToPreview() void FMorphPresetEditorController::ApplyPresetToPreview()
{ {
const UMorphPresetDataAsset* PresetAsset = Preset.Get(); const UMorphPresetDataAsset* PresetAsset = Preset.Get();
@@ -13,7 +13,9 @@
#include "PropertyCustomizationHelpers.h" #include "PropertyCustomizationHelpers.h"
#include "SEditorViewport.h" #include "SEditorViewport.h"
#include "Sound/SoundBase.h" #include "Sound/SoundBase.h"
#include "ToolMenus.h"
#include "Toolkits/AssetEditorToolkit.h" #include "Toolkits/AssetEditorToolkit.h"
#include "ViewportToolbar/UnrealEdViewportToolbar.h"
#include "Widgets/Input/SButton.h" #include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SSlider.h" #include "Widgets/Input/SSlider.h"
@@ -118,11 +120,13 @@ namespace
public: public:
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {} SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller) SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_ARGUMENT(TWeakPtr<FAssetEditorToolkit>, AssetEditorToolkit)
SLATE_END_ARGS() SLATE_END_ARGS()
void Construct(const FArguments& InArgs) void Construct(const FArguments& InArgs)
{ {
Controller = InArgs._Controller; Controller = InArgs._Controller;
AssetEditorToolkit = InArgs._AssetEditorToolkit;
SEditorViewport::Construct(SEditorViewport::FArguments()); SEditorViewport::Construct(SEditorViewport::FArguments());
} }
@@ -146,11 +150,45 @@ namespace
virtual TSharedPtr<SWidget> BuildViewportToolbar() override virtual TSharedPtr<SWidget> BuildViewportToolbar() override
{ {
return SNullWidget::NullWidget; const FName ViewportToolbarName = TEXT("MorphPresetEditor.ViewportToolbar");
if (!UToolMenus::Get()->IsMenuRegistered(ViewportToolbarName))
{
UToolMenu* const ViewportToolbarMenu = UToolMenus::Get()->RegisterMenu(
ViewportToolbarName,
NAME_None,
EMultiBoxType::SlimHorizontalToolBar);
ViewportToolbarMenu->StyleName = TEXT("ViewportToolbar");
FToolMenuSection& LeftSection = ViewportToolbarMenu->AddSection(TEXT("Left"));
LeftSection.AddEntry(UE::UnrealEd::CreateTransformsSubmenu());
LeftSection.AddEntry(UE::UnrealEd::CreateSnappingSubmenu());
FToolMenuSection& RightSection = ViewportToolbarMenu->AddSection(TEXT("Right"));
RightSection.Alignment = EToolMenuSectionAlign::Last;
RightSection.AddEntry(UE::UnrealEd::CreateCameraSubmenu(UE::UnrealEd::FViewportCameraMenuOptions().ShowAll()));
RightSection.AddEntry(UE::UnrealEd::CreateViewModesSubmenu());
RightSection.AddEntry(UE::UnrealEd::CreatePerformanceAndScalabilitySubmenu());
}
FToolMenuContext ViewportToolbarContext;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr)
{
ViewportToolbarContext.AppendCommandList(PreviewScene->GetCommandList());
}
ViewportToolbarContext.AppendCommandList(GetCommandList());
UUnrealEdViewportToolbarContext* const ContextObject =
UE::UnrealEd::CreateViewportToolbarDefaultContext(SharedThis(this));
ContextObject->AssetEditorToolkit = AssetEditorToolkit;
ViewportToolbarContext.AddObject(ContextObject);
return UToolMenus::Get()->GenerateWidget(ViewportToolbarName, ViewportToolbarContext);
} }
private: private:
TSharedPtr<FMorphPresetEditorController> Controller; TSharedPtr<FMorphPresetEditorController> Controller;
TWeakPtr<FAssetEditorToolkit> AssetEditorToolkit;
}; };
} }
@@ -330,6 +368,7 @@ void SMorphPresetViewportPanel::RebuildContent()
[ [
SNew(SMorphPresetPreviewViewport) SNew(SMorphPresetPreviewViewport)
.Controller(Controller) .Controller(Controller)
.AssetEditorToolkit(AssetEditorToolkit)
]); ]);
} }
@@ -8,7 +8,6 @@ class FUICommandList;
class AActor; class AActor;
class IPersonaPreviewScene; class IPersonaPreviewScene;
class IPersonaToolkit; class IPersonaToolkit;
class UAnimationAsset;
class UAnimSequence; class UAnimSequence;
class ULipSyncComponent; class ULipSyncComponent;
class ULipSyncMappingDataAsset; class ULipSyncMappingDataAsset;
@@ -68,9 +67,6 @@ private:
void RebuildPreviewAnimation(); void RebuildPreviewAnimation();
void ApplyPresetToPreview(); void ApplyPresetToPreview();
void RefreshPreviewAnimationPose(); void RefreshPreviewAnimationPose();
UAnimationAsset* ResolvePreviewAnimationAsset() const;
float GetPreviewAnimationPosition() const;
bool IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const;
void RebuildLipSyncPreviewComponents(); void RebuildLipSyncPreviewComponents();
void DestroyLipSyncPreviewComponents(); void DestroyLipSyncPreviewComponents();
void TickLipSyncPreviewComponents(float DeltaTime); void TickLipSyncPreviewComponents(float DeltaTime);
@@ -82,6 +78,7 @@ private:
TSharedPtr<FUICommandList> CommandList; TSharedPtr<FUICommandList> CommandList;
TStrongObjectPtr<UAnimSequence> PreviewAnimation; TStrongObjectPtr<UAnimSequence> PreviewAnimation;
TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping; TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping;
TWeakObjectPtr<USkeletalMesh> ActivePreviewMesh;
TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping; TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping;
TWeakObjectPtr<USoundBase> LipSyncPreviewSound; TWeakObjectPtr<USoundBase> LipSyncPreviewSound;
TWeakObjectPtr<AActor> LipSyncPreviewActor; TWeakObjectPtr<AActor> LipSyncPreviewActor;