diff --git a/AquaMai.Config.Interfaces/IConfigView.cs b/AquaMai.Config.Interfaces/IConfigView.cs index 29094741..49a11a95 100644 --- a/AquaMai.Config.Interfaces/IConfigView.cs +++ b/AquaMai.Config.Interfaces/IConfigView.cs @@ -6,6 +6,7 @@ public interface IConfigView public T GetValueOrDefault(string path, T defaultValue = default); public bool TryGetValue(string path, out T resultValue); public bool Remove(string path); + public bool IsSectionEnabled(string path); public string ToToml(); public IConfigView Clone(); } diff --git a/AquaMai.Config/ConfigView.cs b/AquaMai.Config/ConfigView.cs index 5344aaee..af32c0ea 100644 --- a/AquaMai.Config/ConfigView.cs +++ b/AquaMai.Config/ConfigView.cs @@ -139,4 +139,24 @@ public class ConfigView : IConfigView { return new ConfigView(ToToml()); } + + public bool IsSectionEnabled(string path) + { + if (TryGetValue(path, out object section)) + { + if (section is bool enabled) + { + return enabled; + } + else if (section is TomlTable table) + { + if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disabled)) + { + return !Utility.IsTrutyOrDefault(disabled); + } + return true; + } + } + return false; + } } diff --git a/AquaMai.Config/Migration/ConfigMigrationManager.cs b/AquaMai.Config/Migration/ConfigMigrationManager.cs index fe4ce8f0..2fca298f 100644 --- a/AquaMai.Config/Migration/ConfigMigrationManager.cs +++ b/AquaMai.Config/Migration/ConfigMigrationManager.cs @@ -13,7 +13,8 @@ public class ConfigMigrationManager : IConfigMigrationManager new List { new ConfigMigration_V1_0_V2_0(), - new ConfigMigration_V2_0_V2_1() + new ConfigMigration_V2_0_V2_1(), + new ConfigMigration_V2_1_V2_2(), }.ToDictionary(m => m.FromVersion); public string LatestVersion { get; } diff --git a/AquaMai.Config/Migration/ConfigMigration_V2_0_V2_1.cs b/AquaMai.Config/Migration/ConfigMigration_V2_0_V2_1.cs index e41058ad..0e2f60b4 100644 --- a/AquaMai.Config/Migration/ConfigMigration_V2_0_V2_1.cs +++ b/AquaMai.Config/Migration/ConfigMigration_V2_0_V2_1.cs @@ -13,7 +13,7 @@ public class ConfigMigration_V2_0_V2_1 : IConfigMigration var dst = src.Clone(); dst.SetValue("Version", ToVersion); - if (IsSectionEnabled(src, "Tweaks.ResetTouchAfterTrack")) + if (src.IsSectionEnabled("Tweaks.ResetTouchAfterTrack")) { dst.Remove("Tweaks.ResetTouchAfterTrack"); dst.SetValue("Tweaks.ResetTouch.AfterTrack", true); @@ -21,24 +21,4 @@ public class ConfigMigration_V2_0_V2_1 : IConfigMigration return dst; } - - public bool IsSectionEnabled(IConfigView src, string path) - { - if (src.TryGetValue(path, out object section)) - { - if (section is bool enabled) - { - return enabled; - } - else if (section is TomlTable table) - { - if (Utility.TomlTryGetValueCaseInsensitive(table, "Disabled", out var disabled)) - { - return !Utility.IsTrutyOrDefault(disabled); - } - return true; - } - } - return false; - } } diff --git a/AquaMai.Config/Migration/ConfigMigration_V2_1_V2_2.cs b/AquaMai.Config/Migration/ConfigMigration_V2_1_V2_2.cs new file mode 100644 index 00000000..42fb7b41 --- /dev/null +++ b/AquaMai.Config/Migration/ConfigMigration_V2_1_V2_2.cs @@ -0,0 +1,30 @@ +using AquaMai.Config.Interfaces; +using Tomlet.Models; + +namespace AquaMai.Config.Migration; + +public class ConfigMigration_V2_1_V2_2 : IConfigMigration +{ + public string FromVersion => "2.1"; + public string ToVersion => "2.2"; + + public IConfigView Migrate(IConfigView src) + { + var dst = src.Clone(); + dst.SetValue("Version", ToVersion); + + if (src.IsSectionEnabled("GameSystem.Assets.UseJacketAsDummyMovie")) + { + dst.Remove("GameSystem.Assets.UseJacketAsDummyMovie"); + dst.SetValue("GameSystem.Assets.MovieLoader.JacketAsMovie", true); + } + + if (src.TryGetValue("GameSystem.Assets.LoadLocalImages.LocalAssetsDir", out var localAssetsDir)) + { + dst.SetValue("GameSystem.Assets.LoadLocalImages.ImageAssetsDir", localAssetsDir); + dst.Remove("GameSystem.Assets.LoadLocalImages.LocalAssetsDir"); + } + + return dst; + } +} \ No newline at end of file diff --git a/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs b/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs index 72152de9..d1585815 100644 --- a/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs +++ b/AquaMai.Mods/GameSystem/Assets/LoadLocalImages.cs @@ -20,7 +20,7 @@ namespace AquaMai.Mods.GameSystem.Assets; public class LoadLocalImages { [ConfigEntry] - private static readonly string localAssetsDir = "LocalAssets"; + private static readonly string imageAssetsDir = "LocalAssets"; private static readonly string[] imageExts = [".jpg", ".png", ".jpeg"]; private static readonly Dictionary jacketPaths = []; @@ -122,7 +122,7 @@ public class LoadLocalImages MelonLogger.Msg($"[LoadLocalImages] Loaded {jacketPaths.Count} Jacket, {platePaths.Count} NamePlate, {framePaths.Count} Frame, {framemaskPaths.Count} FrameMask, {framepatternPaths.Count} FramePattern, {iconPaths.Count} Icon, {charaPaths.Count} Chara, {partnerPaths.Count} PartnerLogo, {tabTitlePaths.Count} Tab Titles from AssetBundleImages."); - var resolvedDir = FileSystem.ResolvePath(localAssetsDir); + var resolvedDir = FileSystem.ResolvePath(imageAssetsDir); if (Directory.Exists(resolvedDir)) foreach (var laFile in Directory.EnumerateFiles(resolvedDir)) { diff --git a/AquaMai.Mods/GameSystem/Assets/MovieLoader.cs b/AquaMai.Mods/GameSystem/Assets/MovieLoader.cs new file mode 100644 index 00000000..87bf9349 --- /dev/null +++ b/AquaMai.Mods/GameSystem/Assets/MovieLoader.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.IO; +using AquaMai.Config.Attributes; +using AquaMai.Core.Helpers; +using CriMana; +using HarmonyLib; +using MAI2.Util; +using Manager; +using MelonLoader; +using Monitor.Game; +using UnityEngine; +using UnityEngine.Video; + +namespace AquaMai.Mods.GameSystem.Assets; + +[ConfigSection( + en: "Use mp4 files or the png jacket above as PV if no .dat found in the movie folder.", + zh: "使用封面作为 PV 以及直读 MP4 格式 PV 的选项")] +public class MovieLoader +{ + [ConfigEntry( + en: """ + Use jacket as movie + Use together with `LoadLocalImages`. + """, + zh: """ + 用封面作为背景 PV + 请和 `LoadLocalImages` 一起用 + """)] + private static bool jacketAsMovie = true; + + [ConfigEntry( + en: "Load MP4 files from LocalAssets", + zh: "从 LocalAssets 中加载 MP4 文件作为 PV")] + private static bool loadMp4Movie = true; + + [ConfigEntry( + en: "MP4 files directory", + zh: "加载 MP4 文件的路径")] + private static readonly string movieAssetsDir = "LocalAssets"; + + private static readonly Dictionary optionFileMap = []; + + [HarmonyPrefix] + [HarmonyPatch(typeof(DataManager), "LoadMusicBase")] + public static void LoadMusicPostfix(List ____targetDirs) + { + foreach (var aDir in ____targetDirs) + { + if (!Directory.Exists(Path.Combine(aDir, "MovieData"))) continue; + var files = Directory.GetFiles(Path.Combine(aDir, "MovieData"), "*.mp4"); + foreach (var file in files) + { + optionFileMap[Path.GetFileName(file)] = file; + } + } + } + + private static VideoPlayer[] _videoPlayers = new VideoPlayer[2]; + + [HarmonyPostfix] + [HarmonyPatch(typeof(GameCtrl), "Initialize")] + public static void LoadLocalBgaAwake(GameObject ____movieMaskObj, int ___monitorIndex) + { + var music = Singleton.Instance.GetMusic(GameManager.SelectMusicID[0]); + if (music is null) return; + + var moviePath = Singleton.Instance.GetMovieDataPath($"{music.movieName.id:000000}") + ".dat"; + if (!moviePath.Contains("dummy")) return; + + var resolvedDir = FileSystem.ResolvePath(movieAssetsDir); + if (!optionFileMap.TryGetValue($"{music.movieName.id:000000}.mp4", out var mp4Path)) + { + mp4Path = Path.Combine(resolvedDir, $"{music.movieName.id:000000}.mp4"); + } + + var mp4Exists = File.Exists(mp4Path); + var jacket = LoadLocalImages.GetJacketTexture2D(music.movieName.id); + if (!mp4Exists && jacket is null) + { + MelonLogger.Msg("No jacket found for music " + music); + return; + } + + if (mp4Exists) + { + if (_videoPlayers[___monitorIndex] is null) + { +# if DEBUG + MelonLogger.Msg("Init _videoPlayer"); +# endif + _videoPlayers[___monitorIndex] = ____movieMaskObj.AddComponent(); + } +# if DEBUG + else + { + MelonLogger.Msg("_videoPlayer already exists"); + } +# endif + _videoPlayers[___monitorIndex].url = mp4Path; + _videoPlayers[___monitorIndex].playOnAwake = false; + _videoPlayers[___monitorIndex].renderMode = VideoRenderMode.MaterialOverride; + _videoPlayers[___monitorIndex].audioOutputMode = VideoAudioOutputMode.None; + // 似乎 MaterialOverride 没法保持视频的长宽比,用 RenderTexture 的话放在 SpriteRenderer 里面会比较麻烦。 + // 所以就不保持了,在塞 pv 的时候自己转吧,反正原本也要根据 first 加 padding + } + + var movie = ____movieMaskObj.transform.Find("Movie"); + + // If I create a new RawImage component, the jacket will be not be displayed + // I think it will be difficult to make it work with RawImage + // So I change the material that plays video to default sprite material + // The original player is actually a sprite renderer and plays video with a custom material + var sprite = movie.GetComponent(); + if (mp4Exists) + { + sprite.material = new Material(Shader.Find("Sprites/Default")); + _videoPlayers[___monitorIndex].targetMaterialRenderer = sprite; + } + else + { + sprite.sprite = Sprite.Create(jacket, new Rect(0, 0, jacket.width, jacket.height), new Vector2(0.5f, 0.5f)); + sprite.material = new Material(Shader.Find("Sprites/Default")); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(MovieController), "Play")] + public static void Play(int frame) + { + foreach (var player in _videoPlayers) + { + if (player == null) continue; + player.frame = frame; + player.Play(); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(MovieController), "Pause")] + public static void Pause(bool pauseFlag) + { + foreach (var player in _videoPlayers) + { + if (player == null) continue; + if (pauseFlag) + { + player.Pause(); + } + else + { + player.Play(); + } + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(Player), "SetSpeed")] + public static void SetSpeed(float speed) + { + foreach (var player in _videoPlayers) + { + if (player == null) continue; + player.playbackSpeed = speed; + } + } +} \ No newline at end of file diff --git a/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs b/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs deleted file mode 100644 index a39fb265..00000000 --- a/AquaMai.Mods/GameSystem/Assets/UseJacketAsDummyMovie.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Linq; -using AquaMai.Config.Attributes; -using HarmonyLib; -using MAI2.Util; -using Manager; -using MelonLoader; -using Monitor.Game; -using UnityEngine; - -namespace AquaMai.Mods.GameSystem.Assets; - -[ConfigSection( - en: """ - Use the png jacket above as MV if no .dat found in the movie folder. - Use together with `LoadLocalImages`. - """, - zh: """ - 如果 movie 文件夹中没有 dat 格式的 MV 的话,就用歌曲的封面做背景,而不是显示迪拉熊的笑脸 - 请和 `LoadLocalImages` 一起用 - """)] -public class UseJacketAsDummyMovie -{ - [HarmonyPostfix] - [HarmonyPatch(typeof(GameCtrl), "IsReady")] - public static void LoadLocalBgaAwake(GameObject ____movieMaskObj) - { - var music = Singleton.Instance.GetMusic(GameManager.SelectMusicID[0]); - if (music is null) return; - - var moviePath = Singleton.Instance.GetMovieDataPath($"{music.movieName.id:000000}") + ".dat"; - if (!moviePath.Contains("dummy")) return; - - var jacket = LoadLocalImages.GetJacketTexture2D(music.movieName.id); - if (jacket is null) - { - MelonLogger.Msg("No jacket found for music " + music); - return; - } - - var components = ____movieMaskObj.GetComponentsInChildren(false); - var movies = components.Where(it => it.name == "Movie"); - - foreach (var movie in movies) - { - // If I create a new RawImage component, the jacket will be not be displayed - // I think it will be difficult to make it work with RawImage - // So I change the material that plays video to default sprite material - // The original player is actually a sprite renderer and plays video with a custom material - var sprite = movie.GetComponent(); - sprite.sprite = Sprite.Create(jacket, new Rect(0, 0, jacket.width, jacket.height), new Vector2(0.5f, 0.5f)); - sprite.material = new Material(Shader.Find("Sprites/Default")); - } - } -}