diff --git a/Build/Android/src/com/AIGames/FunnyGirl/OBBData.java b/Build/Android/src/com/AIGames/FunnyGirl/OBBData.java
index f1ce1a1..dac9bf8 100644
--- a/Build/Android/src/com/AIGames/FunnyGirl/OBBData.java
+++ b/Build/Android/src/com/AIGames/FunnyGirl/OBBData.java
@@ -19,7 +19,7 @@ public static final XAPKFile[] xAPKS = {
new XAPKFile(
true, // true signifies a main file
"1", // the version of the APK that the file was uploaded against
-259567652L // the length of the file in bytes
+259568643L // the length of the file in bytes
)
};
};
diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini
index 52da11c..85c6cee 100644
--- a/Config/DefaultEngine.ini
+++ b/Config/DefaultEngine.ini
@@ -1,5 +1,8 @@
+[/Script/Engine.Engine]
++ActiveClassRedirects=(OldClassName="/Script/AndroidRadioPlayer.AndroidRadioPlayerBPLibrary",NewClassName="/Script/FunnyGirl.FunnyGirlUtilityBlueprintLibrary")
+
[/Script/EngineSettings.GameMapsSettings]
EditorStartupMap=/Game/Game/Maps/M_GirlHouse.M_GirlHouse
LocalMapOptions=
diff --git a/Content/Assets/Icon/Chat.uasset b/Content/Assets/Icon/Chat.uasset
new file mode 100644
index 0000000..0f3cefc
--- /dev/null
+++ b/Content/Assets/Icon/Chat.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e926f07e3c0e0ad745d70351e10abb0569486a610e813aa73d1d404ef422d71b
+size 1258949
diff --git a/Content/Assets/Icon/Girl.uasset b/Content/Assets/Icon/Girl.uasset
new file mode 100644
index 0000000..88f79b1
--- /dev/null
+++ b/Content/Assets/Icon/Girl.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1fcb5f346e1a141ff45d8fa45846b4ef9410a23096cebf0bfcd9147646e26443
+size 1275085
diff --git a/Content/Assets/Icon/Player.uasset b/Content/Assets/Icon/Player.uasset
new file mode 100644
index 0000000..e640735
--- /dev/null
+++ b/Content/Assets/Icon/Player.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bee856a3db5e7f602353d272e1154fd9642e29047fe1c838621977c51b07c356
+size 1286162
diff --git a/Content/Assets/Icon/T_Settingss.uasset b/Content/Assets/Icon/T_Settingss.uasset
index 80a6795..8fa5f29 100644
--- a/Content/Assets/Icon/T_Settingss.uasset
+++ b/Content/Assets/Icon/T_Settingss.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56f12d9bb96a8358d4a2de61f9171c0871a9925634ff8b0970b0436c3a36ca11
-size 12331
+oid sha256:36cc006c519082c1fffce52632e62806c7e818c9110ee7cbd1d80815c39ac3db
+size 12479
diff --git a/Content/Game/Blueprints/Girl/Rita/MP_Angry.uasset b/Content/Game/Blueprints/Girl/Rita/MP_Angry.uasset
index ee88cf8..80722ff 100644
--- a/Content/Game/Blueprints/Girl/Rita/MP_Angry.uasset
+++ b/Content/Game/Blueprints/Girl/Rita/MP_Angry.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1117d11b13c81a209550a846cb5c9ca7808b9b1c08c5d6bf5960843c1e423b9d
-size 6633
+oid sha256:90fbb61f7fc632e0fc9e95b5c6bb75263c125bdfa3ad9a15e539059fe1c3da58
+size 6385
diff --git a/Content/Game/Blueprints/Girl/Rita/MP_AngryBurst.uasset b/Content/Game/Blueprints/Girl/Rita/MP_AngryBurst.uasset
index 8f76a76..fe4fd81 100644
--- a/Content/Game/Blueprints/Girl/Rita/MP_AngryBurst.uasset
+++ b/Content/Game/Blueprints/Girl/Rita/MP_AngryBurst.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77cbae55509f6b73f715f366ac2ee28b055c75cf43eb72a2233a8b61e39f5fd2
-size 5978
+oid sha256:13cad6680570d32e430ee633262930fc86c0868de49cc60983e719e888a8ce74
+size 6480
diff --git a/Content/Game/Blueprints/Girl/Rita/MP_Blink.uasset b/Content/Game/Blueprints/Girl/Rita/MP_Blink.uasset
index b6ebecf..95a67ad 100644
--- a/Content/Game/Blueprints/Girl/Rita/MP_Blink.uasset
+++ b/Content/Game/Blueprints/Girl/Rita/MP_Blink.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:37c755e9262480a6efd364f2c567cdb4783a00cd9d988cfdb3434dfce6e4343f
-size 3647
+oid sha256:a04d2519f6e70d5b1f12b384d0b30cd5b57a9847a54eb576581278a93fd87bb6
+size 3994
diff --git a/Content/Game/Blueprints/Girl/Rita/MP_ColdDeadStare.uasset b/Content/Game/Blueprints/Girl/Rita/MP_ColdDeadStare.uasset
index eb2a04b..0fc97e5 100644
--- a/Content/Game/Blueprints/Girl/Rita/MP_ColdDeadStare.uasset
+++ b/Content/Game/Blueprints/Girl/Rita/MP_ColdDeadStare.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20ed2889c7ab894835d867d424e3dc89c698f5d5b9494096849d32c27868d801
-size 5962
+oid sha256:c528b487b5f016e4fc7d587ad4517242ab51febc9bdf432ed1ee0c2f3cbb0bed
+size 6500
diff --git a/Content/Game/Blueprints/Girl/Rita/MP_Idle.uasset b/Content/Game/Blueprints/Girl/Rita/MP_Idle.uasset
index 6b90080..4c52ba9 100644
--- a/Content/Game/Blueprints/Girl/Rita/MP_Idle.uasset
+++ b/Content/Game/Blueprints/Girl/Rita/MP_Idle.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cf7de421cb6754dc21c6581bac590579516327d4282ba799c7967fdc1a6135ee
-size 7556
+oid sha256:308fc5543db3f8fafd5623b39f3d8cb3947157e493a9d7a0be312c4607884841
+size 7564
diff --git a/Content/Game/Blueprints/UI/WBP_Begin.uasset b/Content/Game/Blueprints/UI/WBP_Begin.uasset
index dd17284..273b96a 100644
--- a/Content/Game/Blueprints/UI/WBP_Begin.uasset
+++ b/Content/Game/Blueprints/UI/WBP_Begin.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4c49b0b2130df1e095ba4445c7261a10e7ceb85c6c7c9aaa3e794f113ec6208a
-size 129475
+oid sha256:06cc0c88d219ff8bd0cfc0e9fdf17049766584fd471deaf48b7afbe8af0fb00b
+size 126768
diff --git a/Content/Game/Blueprints/UI/WBP_Button.uasset b/Content/Game/Blueprints/UI/WBP_Button.uasset
index 1bd190b..c711f0f 100644
--- a/Content/Game/Blueprints/UI/WBP_Button.uasset
+++ b/Content/Game/Blueprints/UI/WBP_Button.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:70654c1a13ebd98829c3c6df321f6cfdaacb6dd83921141587163056aee174aa
-size 87138
+oid sha256:1b3bd008dbd9392b693f3893b03b40ce281169bb91c738ef43523f646e2f8abf
+size 87296
diff --git a/Content/Game/Blueprints/UI/WBP_Chat.uasset b/Content/Game/Blueprints/UI/WBP_Chat.uasset
index 6f5212a..d9f2edc 100644
--- a/Content/Game/Blueprints/UI/WBP_Chat.uasset
+++ b/Content/Game/Blueprints/UI/WBP_Chat.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fa19ba00aacc270633cad9ad0706fba09d12aa7cbf9251a686e9bece45864223
-size 246188
+oid sha256:9640acebc7fe51d1d605adb8dba73f798bc905ef9b6a320f6246e7151fa2b76b
+size 256714
diff --git a/Content/Game/Blueprints/UI/WBP_HUD.uasset b/Content/Game/Blueprints/UI/WBP_HUD.uasset
index 5c004eb..9580908 100644
--- a/Content/Game/Blueprints/UI/WBP_HUD.uasset
+++ b/Content/Game/Blueprints/UI/WBP_HUD.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9e4bd64dbe06708aead43df3065694dd76f6634cb4d0e2e844fa8fe7293cb195
-size 74176
+oid sha256:c5c4c38059f3a672a528ea1a3fea4f3d7f63ce54e8a638270cb46b74bda01a28
+size 76264
diff --git a/Content/Game/Blueprints/UI/WBP_Main.uasset b/Content/Game/Blueprints/UI/WBP_Main.uasset
index 2798472..74157c2 100644
--- a/Content/Game/Blueprints/UI/WBP_Main.uasset
+++ b/Content/Game/Blueprints/UI/WBP_Main.uasset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7f32fcb0710d7bd8842b025b7a8dcb396a505f34c93a550f06dd736d54b7c849
-size 79124
+oid sha256:ad9216651944e1c787f213c14776f468a0a30265e4acecf06468f7f17760209a
+size 94500
diff --git a/Content/Game/Blueprints/UI/WBP_Message.uasset b/Content/Game/Blueprints/UI/WBP_Message.uasset
new file mode 100644
index 0000000..31521be
--- /dev/null
+++ b/Content/Game/Blueprints/UI/WBP_Message.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e071f67a994729202c255991d574dfd9a8ac5df3d3cdaac447e8f84e60b25f92
+size 71486
diff --git a/Content/Game/Blueprints/UI/WBP_MessageHistory.uasset b/Content/Game/Blueprints/UI/WBP_MessageHistory.uasset
new file mode 100644
index 0000000..6777ea1
--- /dev/null
+++ b/Content/Game/Blueprints/UI/WBP_MessageHistory.uasset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57780d3f89834e06bac702a7a647b7f524c7537ffd73810a2366b27c17bfbbad
+size 166010
diff --git a/Plugins/AndroidRadioPlayer/AndroidRadioPlayer.uplugin b/Plugins/AndroidRadioPlayer/AndroidRadioPlayer.uplugin
deleted file mode 100644
index 8123092..0000000
--- a/Plugins/AndroidRadioPlayer/AndroidRadioPlayer.uplugin
+++ /dev/null
@@ -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"
- }
- ]
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/AndroidRadioPlayer_UPL.xml b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/AndroidRadioPlayer_UPL.xml
deleted file mode 100644
index 589dd6a..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/AndroidRadioPlayer_UPL.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-dependencies {
- implementation('androidx.media3:media3-exoplayer:1.3.1')
- implementation('androidx.media3:media3-exoplayer-hls:1.3.1')
-}
-
-
-
-
-
-
-
-
-
-
--keep class com.epicgames.unreal.AndroidRadioPlayer {
- public *;
-}
-
-
-
-
-
- AndroidRadioPlayer.handleActivityPause();
-
-
-
-
-
- AndroidRadioPlayer.handleActivityResume();
-
-
-
-
-
- AndroidRadioPlayer.releaseRadio();
-
-
-
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/Java/AndroidRadioPlayer.java b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/Java/AndroidRadioPlayer.java
deleted file mode 100644
index 25345ea..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Android/Java/AndroidRadioPlayer.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/AndroidRadioPlayer.Build.cs b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/AndroidRadioPlayer.Build.cs
deleted file mode 100644
index a22385f..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/AndroidRadioPlayer.Build.cs
+++ /dev/null
@@ -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"));
- }
- }
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.cpp b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.cpp
deleted file mode 100644
index b0115e9..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.cpp
+++ /dev/null
@@ -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(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(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(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(Position);
- }
-#endif
- return 0;
- }
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.h b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.h
deleted file mode 100644
index 4116a5d..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioJavaBridge.h
+++ /dev/null
@@ -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();
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.cpp b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.cpp
deleted file mode 100644
index 1475be5..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.cpp
+++ /dev/null
@@ -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(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& 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 FAndroidRadioMediaPlayer::GetSupportedRates(EMediaRateThinning Thinning) const
-{
- TRangeSet Result;
- Result.Add(TRange(0.0f));
- Result.Add(TRange(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;
- }
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.h b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.h
deleted file mode 100644
index 5f8d677..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioMediaPlayer.h
+++ /dev/null
@@ -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& 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 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;
-};
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerBPLibrary.cpp b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerBPLibrary.cpp
deleted file mode 100644
index 78ccefa..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerBPLibrary.cpp
+++ /dev/null
@@ -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;
-}
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerModule.cpp b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerModule.cpp
deleted file mode 100644
index 171540a..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Private/AndroidRadioPlayerModule.cpp
+++ /dev/null
@@ -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("Media"))
- {
- MediaModule->RegisterPlayerFactory(*this);
- }
- }
-
- virtual void ShutdownModule() override
- {
- if (IMediaModule* MediaModule = FModuleManager::GetModulePtr("Media"))
- {
- MediaModule->UnregisterPlayerFactory(*this);
- }
- }
-
- virtual bool CanPlayUrl(const FString& Url, const IMediaOptions* Options, TArray* OutWarnings, TArray* OutErrors) const override
- {
- return GetPlayabilityConfidenceScore(Url, Options, OutWarnings, OutErrors) > 0;
- }
-
- virtual int32 GetPlayabilityConfidenceScore(const FString& Url, const IMediaOptions* Options, TArray* OutWarnings, TArray* 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 CreatePlayer(IMediaEventSink& EventSink) override
- {
- return MakeShared(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& GetSupportedPlatforms() const override
- {
- return SupportedPlatforms;
- }
-
- virtual bool SupportsFeature(EMediaFeature Feature) const override
- {
- return Feature == EMediaFeature::AudioTracks;
- }
-
-private:
- TArray SupportedPlatforms;
- TArray SupportedUriSchemes;
-};
-
-IMPLEMENT_MODULE(FAndroidRadioPlayerModule, AndroidRadioPlayer)
-
-#undef LOCTEXT_NAMESPACE
diff --git a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Public/AndroidRadioPlayerBPLibrary.h b/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Public/AndroidRadioPlayerBPLibrary.h
deleted file mode 100644
index 8199b8e..0000000
--- a/Plugins/AndroidRadioPlayer/Source/AndroidRadioPlayer/Public/AndroidRadioPlayerBPLibrary.h
+++ /dev/null
@@ -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();
-};
diff --git a/Source/FunnyGirl/FunnyGirl.Build.cs b/Source/FunnyGirl/FunnyGirl.Build.cs
index e5b86cc..c5cb02f 100644
--- a/Source/FunnyGirl/FunnyGirl.Build.cs
+++ b/Source/FunnyGirl/FunnyGirl.Build.cs
@@ -8,7 +8,7 @@ public class FunnyGirl : ModuleRules
{
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" });
diff --git a/Source/FunnyGirl/Private/LLM/PersonaBrainComponent.cpp b/Source/FunnyGirl/Private/LLM/PersonaBrainComponent.cpp
index f2fca42..51d8516 100644
--- a/Source/FunnyGirl/Private/LLM/PersonaBrainComponent.cpp
+++ b/Source/FunnyGirl/Private/LLM/PersonaBrainComponent.cpp
@@ -169,6 +169,87 @@ bool UPersonaBrainComponent::GetLastSavedLLMMessage(FString& OutMessage) const
return false;
}
+TArray UPersonaBrainComponent::GetSavedChatMessages() const
+{
+ TArray ChatMessages;
+
+ const FString FilePath = GetChatFilePath();
+ FString ChatJson;
+ if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
+ {
+ return ChatMessages;
+ }
+
+ TSharedPtr RootObject;
+ TSharedRef> Reader = TJsonReaderFactory<>::Create(ChatJson);
+ if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
+ {
+ return ChatMessages;
+ }
+
+ const TArray>* 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& MessageValue : *SavedMessages)
+ {
+ const TSharedPtr* 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 InteractActionValue = (*MessageObject)->TryGetField(TEXT("interact_action"));
+ if (InteractActionValue.IsValid() && InteractActionValue->Type != EJson::Null)
+ {
+ if (InteractActionValue->Type == EJson::Object)
+ {
+ const TSharedPtr* InteractActionObject = nullptr;
+ if (InteractActionValue->TryGetObject(InteractActionObject) && InteractActionObject && InteractActionObject->IsValid())
+ {
+ FString InteractActionJson;
+ TSharedRef> 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
{
OnLLMError.Broadcast(ErrorMessage);
diff --git a/Source/FunnyGirl/Private/Morph/MorphControllerComponent.cpp b/Source/FunnyGirl/Private/Morph/MorphControllerComponent.cpp
index cb94c37..df597ff 100644
--- a/Source/FunnyGirl/Private/Morph/MorphControllerComponent.cpp
+++ b/Source/FunnyGirl/Private/Morph/MorphControllerComponent.cpp
@@ -2,8 +2,6 @@
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
-#include "Animation/AnimInstance.h"
-#include "Animation/AnimMontage.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Character.h"
#include "Engine/World.h"
@@ -37,6 +35,12 @@ void UMorphControllerComponent::BeginPlay()
Super::BeginPlay();
}
+void UMorphControllerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
+{
+ ClearMorphsImmediately();
+ Super::EndPlay(EndPlayReason);
+}
+
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
@@ -212,7 +216,6 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
if (Slot.Preset == Preset)
{
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
- Slot.CurrentWeight = Slot.TargetWeight;
Slot.PlaybackTime = 0.0f;
Slot.LoopDelayRemaining = 0.0f;
Slot.TimelineDuration = TimelineDuration;
@@ -224,7 +227,7 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
FActiveMorphPresetSlot NewSlot;
NewSlot.Preset = Preset;
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
- NewSlot.CurrentWeight = NewSlot.TargetWeight;
+ NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
NewSlot.TimelineDuration = TimelineDuration;
@@ -239,78 +242,32 @@ bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float
{
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;
}
- const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false;
- 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;
+ return AddPreset(Preset, Weight);
}
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
{
if (!Preset) return;
+ bool bRemovedAny = false;
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
{
if (ActivePresets[i].Preset == Preset)
{
ActivePresets.RemoveAt(i);
+ bRemovedAny = true;
}
}
+
+ if (bRemovedAny)
+ {
+ UpdateMorphTargets(0.0f);
+ }
}
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
@@ -336,6 +293,7 @@ void UMorphControllerComponent::SetActivePresets(const TArrayIsValidPreset())
{
FActiveMorphPresetSlot NewSlot = Slot;
+ NewSlot.TargetWeight = FMath::Clamp(NewSlot.TargetWeight, 0.0f, 1.0f);
NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
@@ -346,11 +304,14 @@ void UMorphControllerComponent::SetActivePresets(const TArray UMorphControllerComponent::GetActivePresets() con
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
{
+ if (MorphTargetName.IsNone())
+ {
+ return;
+ }
+
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
+ UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
{
- DirectMorphOverrides.Remove(MorphTargetName);
+ if (DirectMorphOverrides.Remove(MorphTargetName) > 0)
+ {
+ UpdateMorphTargets(0.0f);
+ }
}
void UMorphControllerComponent::ClearDirectMorphOverrides()
{
DirectMorphOverrides.Empty();
+ UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap& MorphValues)
@@ -405,11 +376,13 @@ void UMorphControllerComponent::SetLipSyncMorphValues(const TMap&
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
}
}
+ UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::ClearLipSyncMorphValues()
{
LipSyncMorphValues.Empty();
+ UpdateMorphTargets(0.0f);
}
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.
TMap 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)
{
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
@@ -508,7 +494,7 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
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.
diff --git a/Source/FunnyGirl/Private/Utility/FunnyGirlUtilityBlueprintLibrary.cpp b/Source/FunnyGirl/Private/Utility/FunnyGirlUtilityBlueprintLibrary.cpp
index 0eca430..beaad78 100644
--- a/Source/FunnyGirl/Private/Utility/FunnyGirlUtilityBlueprintLibrary.cpp
+++ b/Source/FunnyGirl/Private/Utility/FunnyGirlUtilityBlueprintLibrary.cpp
@@ -9,11 +9,13 @@
#include "Engine/SkeletalMesh.h"
#include "Engine/Texture2D.h"
#include "HAL/FileManager.h"
+#include "MediaPlayer.h"
#include "Misc/Base64.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
+#include "StreamMediaSource.h"
namespace
{
@@ -358,3 +360,15 @@ bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field,
SaveObject->RemoveField(EncodedField);
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);
+}
diff --git a/Source/FunnyGirl/Public/LLM/PersonaBrainComponent.h b/Source/FunnyGirl/Public/LLM/PersonaBrainComponent.h
index 660cace..3aa7547 100644
--- a/Source/FunnyGirl/Public/LLM/PersonaBrainComponent.h
+++ b/Source/FunnyGirl/Public/LLM/PersonaBrainComponent.h
@@ -28,6 +28,30 @@ struct FUNNYGIRL_API FLLMReplyResponse
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(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
@@ -85,6 +109,9 @@ public:
UFUNCTION(BlueprintPure, Category = "LLM|History")
bool GetLastSavedLLMMessage(FString& OutMessage) const;
+ UFUNCTION(BlueprintPure, Category = "LLM|History")
+ TArray GetSavedChatMessages() const;
+
virtual void CancelRequest() override;
protected:
diff --git a/Source/FunnyGirl/Public/Morph/MorphControllerComponent.h b/Source/FunnyGirl/Public/Morph/MorphControllerComponent.h
index 55ef84b..a135caf 100644
--- a/Source/FunnyGirl/Public/Morph/MorphControllerComponent.h
+++ b/Source/FunnyGirl/Public/Morph/MorphControllerComponent.h
@@ -74,6 +74,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
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. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
TArray> EmotionPresets;
@@ -178,6 +186,7 @@ public:
protected:
virtual void BeginPlay() override;
+ virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY()
@@ -197,7 +206,6 @@ private:
USkeletalMeshComponent* ResolveMeshComponent() const;
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
- bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
void UpdateMorphTargets(float DeltaTime);
void BlendMorphLayer(const TMap& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap& InOutFinalValues) const;
void ApplyFinalMorphValues(const TMap& FinalValues);
diff --git a/Source/FunnyGirl/Public/Morph/MorphPresetDataAsset.h b/Source/FunnyGirl/Public/Morph/MorphPresetDataAsset.h
index b43e4bb..c02b039 100644
--- a/Source/FunnyGirl/Public/Morph/MorphPresetDataAsset.h
+++ b/Source/FunnyGirl/Public/Morph/MorphPresetDataAsset.h
@@ -8,8 +8,6 @@
#include "Engine/SkeletalMesh.h"
#include "MorphPresetDataAsset.generated.h"
-class UAnimMontage;
-
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FMorphTimelineClip
{
@@ -77,26 +75,6 @@ public:
ClampMin = "0"))
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 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")
int32 GetMorphCount() const;
@@ -104,10 +82,7 @@ public:
float GetTimelineLength() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
- bool HasAnimation() const { return AnimationMontage != nullptr; }
-
- UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
- bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
+ bool HasPlayableContent() const { return IsValidPreset(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool IsValidPreset() const { return GetMorphCount() > 0; }
diff --git a/Source/FunnyGirl/Public/Utility/FunnyGirlUtilityBlueprintLibrary.h b/Source/FunnyGirl/Public/Utility/FunnyGirlUtilityBlueprintLibrary.h
index 3d1d6ff..ab1a6b4 100644
--- a/Source/FunnyGirl/Public/Utility/FunnyGirlUtilityBlueprintLibrary.h
+++ b/Source/FunnyGirl/Public/Utility/FunnyGirlUtilityBlueprintLibrary.h
@@ -8,6 +8,8 @@
class USkeletalMesh;
class UButton;
+class UMediaPlayer;
+class UStreamMediaSource;
class UTexture2D;
UENUM(BlueprintType)
@@ -53,4 +55,7 @@ public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool DeleteLightString(const FString& Field, FString& OutError);
+
+ UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Media")
+ static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
};
diff --git a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetDetailsCustomization.cpp b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetDetailsCustomization.cpp
index 7fa7844..812a9f5 100644
--- a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetDetailsCustomization.cpp
+++ b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetDetailsCustomization.cpp
@@ -64,10 +64,6 @@ namespace
TargetPreset.bLoop = SourcePreset.bLoop;
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
- TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
- TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
- TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
- TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
}
}
diff --git a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetEditorController.cpp b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetEditorController.cpp
index 8c36ee8..fe461ed 100644
--- a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetEditorController.cpp
+++ b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetEditorController.cpp
@@ -4,10 +4,7 @@
#include "Animation/AnimData/IAnimationDataController.h"
#include "AnimPreviewInstance.h"
-#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
-#include "Animation/AnimSequenceBase.h"
-#include "Animation/AnimationAsset.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Animation/Skeleton.h"
#include "Components/SkeletalMeshComponent.h"
@@ -115,15 +112,7 @@ float FMorphPresetEditorController::GetTimelineLength() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
- float TimelineLength = PresetAsset->GetTimelineLength();
- 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 FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
}
return 1.0f;
@@ -145,7 +134,7 @@ void FMorphPresetEditorController::SetCurrentTime(float InTime)
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
- PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
+ PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
}
}
@@ -225,20 +214,21 @@ void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChang
PresetAsset->MarkPackageDirty();
}
- UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
- UAnimationAsset* CurrentPreviewAsset = nullptr;
- if (TSharedPtr PreviewScene = GetPreviewScene())
+ if (ActivePreviewMesh.Get() != GetPreviewMesh())
{
- CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
+ RebuildPreviewScene();
+ if (bBroadcastPresetChanged)
+ {
+ PresetChanged.Broadcast();
+ }
+ return;
}
- if (DesiredPreviewAsset != CurrentPreviewAsset ||
- (DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
- (!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
+ if (!PreviewAnimation.IsValid())
{
RebuildPreviewAnimation();
}
- else if (PreviewAnimation.IsValid())
+ else
{
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
}
@@ -436,6 +426,7 @@ void FMorphPresetEditorController::RebuildPreviewScene()
PreviewAnimation.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh();
+ ActivePreviewMesh = PreviewMesh;
if (PreviewMesh == nullptr)
{
PreviewSceneChanged.Broadcast();
@@ -464,56 +455,29 @@ void FMorphPresetEditorController::RebuildPreviewAnimation()
PreviewAnimation.Reset();
PreviewStatusText = FText::GetEmpty();
- UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
- if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
+ PreviewAnimation = TStrongObjectPtr(AnimSequence);
+ SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
+ if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
{
- const USkeletalMesh* PreviewMesh = GetPreviewMesh();
- 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(AnimSequence);
- PreviewAsset = AnimSequence;
-
- SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
- if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
+ if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
{
- if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
- {
- AnimSequence->SetSkeleton(Skeleton);
- }
+ AnimSequence->SetSkeleton(Skeleton);
}
}
- PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true);
+ PreviewScene->SetPreviewAnimationAsset(AnimSequence, true);
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
if (PreviewMeshComponent->PreviewInstance)
{
- const UMorphPresetDataAsset* PresetAsset = Preset.Get();
- const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
- PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
+ PreviewMeshComponent->PreviewInstance->SetAnimationAsset(AnimSequence, false, 1.0f);
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetLooping(false);
- PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
+ PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
}
}
@@ -539,52 +503,6 @@ void FMorphPresetEditorController::RefreshPreviewAnimationPose()
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(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()
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
diff --git a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetViewportPanel.cpp b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetViewportPanel.cpp
index 762e53a..1a7fede 100644
--- a/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetViewportPanel.cpp
+++ b/Source/FunnyGirlEditor/Private/Morph/Editor/MorphPresetViewportPanel.cpp
@@ -13,7 +13,9 @@
#include "PropertyCustomizationHelpers.h"
#include "SEditorViewport.h"
#include "Sound/SoundBase.h"
+#include "ToolMenus.h"
#include "Toolkits/AssetEditorToolkit.h"
+#include "ViewportToolbar/UnrealEdViewportToolbar.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SSlider.h"
@@ -118,11 +120,13 @@ namespace
public:
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
SLATE_ARGUMENT(TSharedPtr, Controller)
+ SLATE_ARGUMENT(TWeakPtr, AssetEditorToolkit)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
+ AssetEditorToolkit = InArgs._AssetEditorToolkit;
SEditorViewport::Construct(SEditorViewport::FArguments());
}
@@ -146,11 +150,45 @@ namespace
virtual TSharedPtr 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 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:
TSharedPtr Controller;
+ TWeakPtr AssetEditorToolkit;
};
}
@@ -330,6 +368,7 @@ void SMorphPresetViewportPanel::RebuildContent()
[
SNew(SMorphPresetPreviewViewport)
.Controller(Controller)
+ .AssetEditorToolkit(AssetEditorToolkit)
]);
}
diff --git a/Source/FunnyGirlEditor/Public/Morph/Editor/MorphPresetEditorController.h b/Source/FunnyGirlEditor/Public/Morph/Editor/MorphPresetEditorController.h
index b47bc54..6179634 100644
--- a/Source/FunnyGirlEditor/Public/Morph/Editor/MorphPresetEditorController.h
+++ b/Source/FunnyGirlEditor/Public/Morph/Editor/MorphPresetEditorController.h
@@ -8,7 +8,6 @@ class FUICommandList;
class AActor;
class IPersonaPreviewScene;
class IPersonaToolkit;
-class UAnimationAsset;
class UAnimSequence;
class ULipSyncComponent;
class ULipSyncMappingDataAsset;
@@ -68,9 +67,6 @@ private:
void RebuildPreviewAnimation();
void ApplyPresetToPreview();
void RefreshPreviewAnimationPose();
- UAnimationAsset* ResolvePreviewAnimationAsset() const;
- float GetPreviewAnimationPosition() const;
- bool IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const;
void RebuildLipSyncPreviewComponents();
void DestroyLipSyncPreviewComponents();
void TickLipSyncPreviewComponents(float DeltaTime);
@@ -82,6 +78,7 @@ private:
TSharedPtr CommandList;
TStrongObjectPtr PreviewAnimation;
TStrongObjectPtr DefaultLipSyncPreviewMapping;
+ TWeakObjectPtr ActivePreviewMesh;
TWeakObjectPtr LipSyncPreviewMapping;
TWeakObjectPtr LipSyncPreviewSound;
TWeakObjectPtr LipSyncPreviewActor;